gloomberb 0.11.0 → 0.11.2

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 (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -340,6 +340,83 @@ describe("prediction markets pane interactions", () => {
340
340
  expect(frame).toContain("/ search markets");
341
341
  });
342
342
 
343
+ test("navigates the book and trades tables with arrow keys", async () => {
344
+ installPredictionMarketMocks();
345
+
346
+ testSetup = await testRender(<Harness />, { width: 120, height: 34 });
347
+ await flushFrames(testSetup);
348
+ await emitKeypress(testSetup, { name: "j", sequence: "j" });
349
+ await emitKeypress(testSetup, { name: "enter", sequence: "\r" });
350
+ await flushFrames(testSetup);
351
+
352
+ let rows = testSetup.captureCharFrame().split("\n");
353
+ const detailTabsRow = rows.findIndex((line) =>
354
+ line.includes("Overview") && line.includes("Book") && line.includes("Trades"),
355
+ );
356
+ const bookColumn = rows[detailTabsRow]!.indexOf("Book");
357
+ await act(async () => {
358
+ await testSetup!.mockMouse.click(bookColumn, detailTabsRow);
359
+ await testSetup!.renderOnce();
360
+ });
361
+ await flushFrames(testSetup);
362
+
363
+ rows = testSetup.captureCharFrame().split("\n");
364
+ let headerRow = rows.findIndex((line) =>
365
+ line.includes("OUT") && line.includes("SIDE") && line.includes("PRICE"),
366
+ );
367
+ expect(headerRow).toBeGreaterThanOrEqual(0);
368
+ let spans = testSetup.captureSpans().lines;
369
+ const bookFirstBefore = spans[headerRow + 1]!.spans.find(
370
+ (span) => span.text.trim(),
371
+ )!.bg;
372
+ const bookSecondBefore = spans[headerRow + 2]!.spans.find(
373
+ (span) => span.text.trim(),
374
+ )!.bg;
375
+ expect(bookFirstBefore).not.toEqual(bookSecondBefore);
376
+
377
+ await emitKeypress(testSetup, { name: "down", sequence: "\u001b[B" });
378
+ await flushFrames(testSetup);
379
+ spans = testSetup.captureSpans().lines;
380
+ expect(
381
+ spans[headerRow + 1]!.spans.find((span) => span.text.trim())!.bg,
382
+ ).toEqual(bookSecondBefore);
383
+ expect(
384
+ spans[headerRow + 2]!.spans.find((span) => span.text.trim())!.bg,
385
+ ).toEqual(bookFirstBefore);
386
+
387
+ rows = testSetup.captureCharFrame().split("\n");
388
+ const tradesColumn = rows[detailTabsRow]!.indexOf("Trades");
389
+ await act(async () => {
390
+ await testSetup!.mockMouse.click(tradesColumn, detailTabsRow);
391
+ await testSetup!.renderOnce();
392
+ });
393
+ await flushFrames(testSetup);
394
+
395
+ rows = testSetup.captureCharFrame().split("\n");
396
+ headerRow = rows.findIndex((line) =>
397
+ line.includes("TIME") && line.includes("SIDE") && line.includes("PRICE"),
398
+ );
399
+ expect(headerRow).toBeGreaterThanOrEqual(0);
400
+ spans = testSetup.captureSpans().lines;
401
+ const tradeFirstBefore = spans[headerRow + 1]!.spans.find(
402
+ (span) => span.text.trim(),
403
+ )!.bg;
404
+ const tradeSecondBefore = spans[headerRow + 2]!.spans.find(
405
+ (span) => span.text.trim(),
406
+ )!.bg;
407
+ expect(tradeFirstBefore).not.toEqual(tradeSecondBefore);
408
+
409
+ await emitKeypress(testSetup, { name: "down", sequence: "\u001b[B" });
410
+ await flushFrames(testSetup);
411
+ spans = testSetup.captureSpans().lines;
412
+ expect(
413
+ spans[headerRow + 1]!.spans.find((span) => span.text.trim())!.bg,
414
+ ).toEqual(tradeSecondBefore);
415
+ expect(
416
+ spans[headerRow + 2]!.spans.find((span) => span.text.trim())!.bg,
417
+ ).toEqual(tradeFirstBefore);
418
+ });
419
+
343
420
  test("supports detail outcome navigation and escape return from the keyboard", async () => {
344
421
  attachPredictionMarketsPersistence(new MemoryPersistence());
345
422
 
@@ -5,6 +5,7 @@ import {
5
5
  Spinner,
6
6
  Tabs,
7
7
  usePaneFooter,
8
+ useTableLoadMore,
8
9
  type DataTableKeyEvent,
9
10
  type DataTableRootKeyContext,
10
11
  } from "../../components";
@@ -238,6 +239,12 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
238
239
  watchlistedRowKeys,
239
240
  ]);
240
241
 
242
+ const onCatalogScroll = useTableLoadMore(
243
+ controller.scrollRef,
244
+ controller.catalogHasMore && !controller.catalogLoadingMore && !controller.detailOpen,
245
+ () => { void controller.actions.loadMoreCatalog(); },
246
+ );
247
+
241
248
  const handleRootKeyDown = useCallback((
242
249
  event: DataTableKeyEvent,
243
250
  context: DataTableRootKeyContext,
@@ -255,9 +262,13 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
255
262
  <Box
256
263
  flexDirection="column"
257
264
  flexGrow={1}
265
+ flexShrink={1}
266
+ flexBasis={0}
267
+ minHeight={0}
258
268
  width={width}
259
269
  height={Math.max(height - 1, 1)}
260
270
  paddingX={1}
271
+ overflow="hidden"
261
272
  backgroundColor={colors.panel}
262
273
  >
263
274
  <PredictionMarketDetailPane
@@ -314,6 +325,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
314
325
  scrollRef={controller.scrollRef}
315
326
  getItemKey={(row) => row.key}
316
327
  virtualize
328
+ onBodyScrollActivity={onCatalogScroll}
317
329
  renderCell={renderCell}
318
330
  emptyContent={
319
331
  rowsLoading ? (
@@ -1,4 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
+ import { createDefaultConfig } from "../../types/config";
3
+ import type { CommandDef } from "../../types/plugin";
2
4
  import {
3
5
  cleanupPredictionTest,
4
6
  installPredictionMarketMocks,
@@ -23,12 +25,46 @@ import {
23
25
  loadPolymarketDetail,
24
26
  normalizePolymarketMarket,
25
27
  } from "./services/polymarket/adapter";
28
+ import { predictionMarketsPlugin } from "./index";
26
29
 
27
30
  afterEach(async () => {
28
31
  await cleanupPredictionTest();
29
32
  });
30
33
 
31
34
  describe("prediction markets plugin registration and services", () => {
35
+ test("routes search state to a materialized pane instance", async () => {
36
+ const config = createDefaultConfig("/tmp/gloomberb-prediction-command-test");
37
+ const instanceId = "prediction-markets:shared-test";
38
+ config.layout.instances.push({ instanceId, paneId: "prediction-markets" });
39
+ const commands: CommandDef[] = [];
40
+ const paneStateUpdates: Array<{ paneId: string; key: string; value: unknown }> = [];
41
+ const focused: string[] = [];
42
+
43
+ predictionMarketsPlugin.setup?.({
44
+ persistence: new MemoryPersistence(),
45
+ getConfig: () => config,
46
+ resume: {
47
+ setPaneState(paneId: string, key: string, value: unknown) {
48
+ paneStateUpdates.push({ paneId, key, value });
49
+ },
50
+ },
51
+ registerCommand: (command: CommandDef) => commands.push(command),
52
+ focusPane: (paneId: string) => focused.push(paneId),
53
+ } as any);
54
+
55
+ await commands.find((command) => command.id === "prediction-markets-search")?.execute({
56
+ query: "polymarket:fed",
57
+ });
58
+
59
+ expect(paneStateUpdates).toEqual([
60
+ { paneId: instanceId, key: "venueScope", value: "polymarket" },
61
+ { paneId: instanceId, key: "searchQuery", value: "fed" },
62
+ { paneId: instanceId, key: "selectedRowKey", value: null },
63
+ { paneId: instanceId, key: "selectedDetailMarketKey", value: null },
64
+ ]);
65
+ expect(focused).toEqual([instanceId]);
66
+ });
67
+
32
68
  test("uses fresh prediction resource cache without refetching", async () => {
33
69
  const persistence = new MemoryPersistence();
34
70
  attachPredictionMarketsPersistence(persistence);
@@ -39,6 +39,23 @@ export { normalizeKalshiMarket } from "./normalize";
39
39
  const KALSHI_EVENT_PAGE_LIMIT = 200;
40
40
  const DEFAULT_KALSHI_EVENT_MAX_PAGES = 3;
41
41
  const SEARCH_KALSHI_EVENT_MAX_PAGES = 3;
42
+ const kalshiCursors = new Map<string, string | null>();
43
+
44
+ function kalshiCursorKey(searchQuery: string, categoryId: PredictionCategoryId): string {
45
+ return `${categoryId}:${searchQuery.trim().toLowerCase()}`;
46
+ }
47
+
48
+ function rememberKalshiCursor(
49
+ searchQuery: string,
50
+ categoryId: PredictionCategoryId,
51
+ cursor: string | null,
52
+ ): void {
53
+ kalshiCursors.set(kalshiCursorKey(searchQuery, categoryId), cursor);
54
+ }
55
+
56
+ export function kalshiCatalogCursor(searchQuery: string, categoryId: PredictionCategoryId): string | null {
57
+ return kalshiCursors.get(kalshiCursorKey(searchQuery, categoryId)) ?? null;
58
+ }
42
59
 
43
60
  function kalshiSeriesTickerFromEvent(eventTicker: string | undefined): string | undefined {
44
61
  const trimmed = eventTicker?.trim().toUpperCase();
@@ -61,9 +78,10 @@ async function fetchKalshiCatalogEvents(
61
78
  maxPages = DEFAULT_KALSHI_EVENT_MAX_PAGES,
62
79
  limit = KALSHI_EVENT_PAGE_LIMIT,
63
80
  signal?: AbortSignal,
64
- ): Promise<KalshiEventRecord[]> {
81
+ startCursor?: string,
82
+ ): Promise<{ events: KalshiEventRecord[]; nextCursor: string | null }> {
65
83
  const events: KalshiEventRecord[] = [];
66
- let cursor: string | undefined;
84
+ let cursor: string | undefined = startCursor;
67
85
 
68
86
  for (let page = 0; page < maxPages; page += 1) {
69
87
  const response = await fetchJson<KalshiEventsResponse>(
@@ -75,7 +93,7 @@ async function fetchKalshiCatalogEvents(
75
93
  if (!cursor) break;
76
94
  }
77
95
 
78
- return events;
96
+ return { events, nextCursor: cursor ?? null };
79
97
  }
80
98
 
81
99
  async function fetchKalshiCatalogEventsForCategory(
@@ -83,13 +101,15 @@ async function fetchKalshiCatalogEventsForCategory(
83
101
  maxPages = DEFAULT_KALSHI_EVENT_MAX_PAGES,
84
102
  limit = KALSHI_EVENT_PAGE_LIMIT,
85
103
  signal?: AbortSignal,
86
- ): Promise<KalshiEventRecord[]> {
104
+ startCursor?: string,
105
+ ): Promise<{ events: KalshiEventRecord[]; nextCursor: string | null }> {
87
106
  const categories = getKalshiCategoryNames(categoryId);
88
- if (categories.length === 0) return await fetchKalshiCatalogEvents(maxPages, limit, signal);
107
+ if (categories.length === 0) return await fetchKalshiCatalogEvents(maxPages, limit, signal, startCursor);
89
108
 
90
109
  const deduped = new Map<string, KalshiEventRecord>();
110
+ let nextCursor: string | null = null;
91
111
  for (const category of categories) {
92
- let cursor: string | undefined;
112
+ let cursor: string | undefined = startCursor;
93
113
  for (let page = 0; page < maxPages; page += 1) {
94
114
  const response = await fetchJson<KalshiEventsResponse>(
95
115
  buildKalshiCatalogUrl(cursor, category, limit),
@@ -102,9 +122,10 @@ async function fetchKalshiCatalogEventsForCategory(
102
122
  cursor = response.cursor?.trim() || undefined;
103
123
  if (!cursor) break;
104
124
  }
125
+ nextCursor = cursor ?? nextCursor;
105
126
  }
106
127
 
107
- return [...deduped.values()];
128
+ return { events: [...deduped.values()], nextCursor };
108
129
  }
109
130
 
110
131
  export async function loadKalshiCatalog(
@@ -120,15 +141,34 @@ export async function loadKalshiCatalog(
120
141
  "catalog",
121
142
  `${buildPredictionCatalogResourceKey("kalshi", categoryId, normalizedQuery)}:${requestedLimit}`,
122
143
  async () => {
123
- const events = categoryId === "all"
144
+ const page = categoryId === "all"
124
145
  ? await fetchKalshiCatalogEvents(maxPages, pageLimit, options.signal)
125
146
  : await fetchKalshiCatalogEventsForCategory(categoryId, maxPages, pageLimit, options.signal);
126
- return normalizeKalshiCatalog(events, normalizedQuery, categoryId).slice(0, requestedLimit);
147
+ rememberKalshiCursor(normalizedQuery, categoryId, page.nextCursor);
148
+ return normalizeKalshiCatalog(page.events, normalizedQuery, categoryId).slice(0, requestedLimit);
127
149
  },
128
150
  PREDICTION_CACHE_POLICIES.catalog,
129
151
  );
130
152
  }
131
153
 
154
+ export async function loadMoreKalshiCatalog(
155
+ searchQuery: string,
156
+ categoryId: PredictionCategoryId,
157
+ cursor: string,
158
+ signal?: AbortSignal,
159
+ ): Promise<{ markets: PredictionMarketSummary[]; nextCursor: string | null; hasMore: boolean }> {
160
+ const normalizedQuery = searchQuery.trim().toLowerCase();
161
+ const page = categoryId === "all"
162
+ ? await fetchKalshiCatalogEvents(1, KALSHI_EVENT_PAGE_LIMIT, signal, cursor)
163
+ : await fetchKalshiCatalogEventsForCategory(categoryId, 1, KALSHI_EVENT_PAGE_LIMIT, signal, cursor);
164
+ rememberKalshiCursor(normalizedQuery, categoryId, page.nextCursor);
165
+ return {
166
+ markets: normalizeKalshiCatalog(page.events, normalizedQuery, categoryId),
167
+ nextCursor: page.nextCursor,
168
+ hasMore: !!page.nextCursor,
169
+ };
170
+ }
171
+
132
172
  async function loadKalshiEvent(
133
173
  eventTicker: string | undefined,
134
174
  ): Promise<KalshiEventResponse | null> {
@@ -30,6 +30,16 @@ export { loadPolymarketDetail } from "./detail";
30
30
 
31
31
  const POLYMARKET_CATALOG_OFFSETS = [0, 200, 400];
32
32
  const POLYMARKET_CATEGORY_OFFSETS = [0, 200];
33
+ const POLYMARKET_PAGE_SIZE = 200;
34
+
35
+ export function nextPolymarketCatalogOffset(
36
+ categoryId: PredictionCategoryId,
37
+ searchQuery = "",
38
+ ): number | null {
39
+ if (searchQuery.trim()) return null;
40
+ const offsets = categoryId === "all" ? POLYMARKET_CATALOG_OFFSETS : POLYMARKET_CATEGORY_OFFSETS;
41
+ return (offsets.at(-1) ?? 0) + POLYMARKET_PAGE_SIZE;
42
+ }
33
43
 
34
44
  function buildPolymarketCatalogUrl(offset: number, tagSlug?: string, limit = 200): string {
35
45
  const url = new URL("https://gamma-api.polymarket.com/events");
@@ -144,3 +154,24 @@ export async function loadPolymarketCatalog(
144
154
  PREDICTION_CACHE_POLICIES.catalog,
145
155
  );
146
156
  }
157
+
158
+ export async function loadMorePolymarketCatalog(
159
+ searchQuery: string,
160
+ categoryId: PredictionCategoryId,
161
+ offset: number,
162
+ signal?: AbortSignal,
163
+ ): Promise<{ markets: PredictionMarketSummary[]; hasMore: boolean; nextOffset: number }> {
164
+ if (searchQuery.trim()) {
165
+ return { markets: [], hasMore: false, nextOffset: offset };
166
+ }
167
+ const tagSlugs = categoryId === "all" ? [undefined] : getPolymarketCategoryTagSlugs(categoryId);
168
+ const pages = await Promise.all(
169
+ tagSlugs.map((tagSlug) => loadPolymarketCatalogPages([offset], tagSlug, POLYMARKET_PAGE_SIZE, signal).catch(() => [])),
170
+ );
171
+ const raw = pages.flat();
172
+ return {
173
+ markets: normalizePolymarketCatalog(raw, "", categoryId),
174
+ hasMore: raw.length >= POLYMARKET_PAGE_SIZE,
175
+ nextOffset: offset + POLYMARKET_PAGE_SIZE,
176
+ };
177
+ }
@@ -274,6 +274,15 @@ export function installPredictionMarketMocks() {
274
274
  count_fp: "50",
275
275
  created_time: "2026-04-01T00:00:00Z",
276
276
  },
277
+ {
278
+ trade_id: "kal-2",
279
+ ticker: "KAL-1",
280
+ taker_side: "no",
281
+ yes_price_dollars: "0.46",
282
+ no_price_dollars: "0.54",
283
+ count_fp: "40",
284
+ created_time: "2026-04-01T00:01:00Z",
285
+ },
277
286
  ],
278
287
  }),
279
288
  { status: 200 },
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { AppPersistencePort, AppTickerRepositoryPort } from "../../core/app-service-ports";
3
+ import type { LayoutMarketplacePayload } from "../../layout-marketplace/payload";
3
4
  import {
4
5
  connectionHealth as sharedConnectionHealth,
5
6
  type ConnectionHealthRegistry,
@@ -129,6 +130,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
129
130
  showPaneFn: ((paneId: string) => void) = () => {};
130
131
  createPaneFromTemplateFn: ((templateId: string, options?: PaneTemplateCreateOptions) => void) = () => {};
131
132
  createPaneFromTemplateAsyncFn: ((templateId: string, options?: PaneTemplateCreateOptions) => Promise<void>) = async () => {};
133
+ openPortablePaneShareAsyncFn: ((layout: LayoutMarketplacePayload) => Promise<void>) = async () => {};
132
134
  hidePaneFn: ((paneId: string) => void) = () => {};
133
135
  focusPaneFn: ((paneId: string) => void) = () => {};
134
136
  pinTickerFn: ((symbol: string, options?: PinTickerOptions) => void) = () => {};
@@ -7,6 +7,8 @@ import {
7
7
  } from "../types/config";
8
8
  import {
9
9
  findFixedTickerPaneForSymbol,
10
+ findTickerResearchFollower,
11
+ resolveTickerActivation,
10
12
  resolveTickerNavigationReplacementPane,
11
13
  shouldFocusTickerNavigationTarget,
12
14
  } from "./ticker-navigation";
@@ -26,6 +28,28 @@ function createLayout(instances: PaneInstanceConfig[]): LayoutConfig {
26
28
  };
27
29
  }
28
30
 
31
+ describe("ticker source activation", () => {
32
+ test("focuses a bound follower unless the caller asks for a new pane", () => {
33
+ const layout = createLayout([
34
+ createPaneInstance("portfolio-list", {
35
+ instanceId: "portfolio-list:main",
36
+ binding: { kind: "none" },
37
+ }),
38
+ createPaneInstance("ticker-detail", {
39
+ instanceId: "ticker-detail:main",
40
+ binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
41
+ }),
42
+ ]);
43
+
44
+ expect(findTickerResearchFollower(layout, "portfolio-list:main")?.instanceId).toBe("ticker-detail:main");
45
+ expect(resolveTickerActivation(layout, "portfolio-list:main")).toEqual({
46
+ kind: "focus",
47
+ paneId: "ticker-detail:main",
48
+ });
49
+ expect(resolveTickerActivation(layout, "portfolio-list:main", { newPane: true })).toEqual({ kind: "open" });
50
+ });
51
+ });
52
+
29
53
  describe("resolveTickerNavigationReplacementPane", () => {
30
54
  test("only replaces the Ticker Research pane that opened ticker navigation", () => {
31
55
  const layout = createLayout([
@@ -5,8 +5,48 @@ import {
5
5
  type LayoutConfig,
6
6
  type PaneInstanceConfig,
7
7
  } from "../types/config";
8
+ import type { PaneDef } from "../types/plugin";
8
9
  import { isPaneInLayout } from "./pane-manager";
9
10
 
11
+ /** Panes that publish a cursor symbol other panes can follow (`PaneDef.tickerSource`). */
12
+ export function listVisibleTickerSourcePanes(
13
+ layout: LayoutConfig,
14
+ panes: ReadonlyMap<string, PaneDef>,
15
+ ): PaneInstanceConfig[] {
16
+ return layout.instances.filter((instance) => (
17
+ panes.get(instance.paneId)?.tickerSource === true
18
+ && isPaneInLayout(layout, instance.instanceId)
19
+ ));
20
+ }
21
+
22
+ /** The visible Ticker Research pane that follows `sourceInstanceId`, if any. */
23
+ export function findTickerResearchFollower(
24
+ layout: LayoutConfig,
25
+ sourceInstanceId: string | null | undefined,
26
+ ): PaneInstanceConfig | null {
27
+ if (!sourceInstanceId) return null;
28
+ return layout.instances.find((instance) =>
29
+ instance.paneId === TICKER_RESEARCH_PANE_ID
30
+ && instance.binding?.kind === "follow"
31
+ && instance.binding.sourceInstanceId === sourceInstanceId
32
+ && isPaneInLayout(layout, instance.instanceId)
33
+ ) ?? null;
34
+ }
35
+
36
+ /**
37
+ * Enter opens the follower already bound to the source pane; Shift+Enter (`newPane`) and sources
38
+ * without a follower fall back to opening a ticker pane.
39
+ */
40
+ export function resolveTickerActivation(
41
+ layout: LayoutConfig,
42
+ sourcePaneId: string | null | undefined,
43
+ options?: { newPane?: boolean },
44
+ ): { kind: "focus"; paneId: string } | { kind: "open" } {
45
+ if (options?.newPane) return { kind: "open" };
46
+ const follower = findTickerResearchFollower(layout, sourcePaneId);
47
+ return follower ? { kind: "focus", paneId: follower.instanceId } : { kind: "open" };
48
+ }
49
+
10
50
  export function resolveTickerNavigationReplacementPane(
11
51
  layout: LayoutConfig,
12
52
  sourcePaneId: string | null,
@@ -43,7 +43,7 @@ export const REMOTE_OPERATIONS: RemoteOperationSchema[] = [
43
43
  op("layout.delete", "Delete a layout.", "{ index: number }", "local-write"),
44
44
  op("layout.undo", "Undo last layout change.", "{}", "local-write"),
45
45
  op("layout.redo", "Redo last layout change.", "{}", "local-write"),
46
- op("layout.gridlock", "Gridlock all visible panes into a dense layout.", "{}", "local-write"),
46
+ op("layout.gridlock", "Tidy all panes into a dense layout.", "{}", "local-write"),
47
47
  op("layout.closeFloating", "Close all floating panes in the active layout.", "{}", "local-write"),
48
48
  op("layout.placePane", "Move a pane to a layout region.", "{ paneId: string, region: 'left' | 'right' | 'top' | 'bottom' | 'floating', relativeTo?: string }", "local-write"),
49
49
  op("layout.focusRegion", "Focus a pane by visual layout region.", "{ region: 'left' | 'right' | 'top' | 'bottom' | 'center' }", "local-write"),
@@ -0,0 +1,61 @@
1
+ import { newsProvider, type NewsCapability } from "../../capabilities";
2
+ import type { AppRuntimeServices, AppServicesFactoryOptions } from "../../core/app-service-ports";
3
+ import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
4
+ import { NewsService } from "../../news/aggregator";
5
+ import { setSharedNewsService } from "../../news/hooks";
6
+ import { PluginRegistry } from "../../plugins/registry";
7
+ import { createGloomberbCloudCapabilities, createGloomberbCloudProvider } from "../../sources/gloomberb-cloud";
8
+ import { AssetDataRouter } from "../../sources/provider-router";
9
+ import { BrowserPersistence } from "./persistence";
10
+ import { BrowserTickerRepository } from "./ticker-repository";
11
+
12
+ export function createBrowserAppServices({
13
+ config,
14
+ plugins,
15
+ }: AppServicesFactoryOptions): AppRuntimeServices {
16
+ const persistence = new BrowserPersistence(localStorage);
17
+ const tickerRepository = new BrowserTickerRepository(localStorage);
18
+ const cloudProvider = createGloomberbCloudProvider();
19
+ const dataProvider = new AssetDataRouter(null, [cloudProvider]);
20
+ const marketData = new MarketDataCoordinator(dataProvider);
21
+ const cloudNews = createGloomberbCloudCapabilities(cloudProvider).find(
22
+ (capability): capability is NewsCapability => capability.kind === "news",
23
+ );
24
+ const pluginRegistry = new PluginRegistry(dataProvider, tickerRepository, persistence);
25
+ dataProvider.attachRegistry(pluginRegistry);
26
+ const newsService = new NewsService({
27
+ pollIntervalMs: () => Math.max(1, config.refreshIntervalMinutes) * 60_000,
28
+ });
29
+
30
+ pluginRegistry.getConfigFn = () => config;
31
+ pluginRegistry.getLayoutFn = () => config.layout;
32
+ pluginRegistry.registerNewsCapabilityFn = (capability) => newsService.register(capability);
33
+ pluginRegistry.watchNewsQueryFn = (query, listener) => newsService.watchQuery(query, listener);
34
+ newsService.register(newsProvider({
35
+ id: dataProvider.id,
36
+ name: dataProvider.name,
37
+ priority: 0,
38
+ provider: { fetchNews: (query) => cloudNews?.provider.fetchNews(query) ?? Promise.resolve([]) },
39
+ }));
40
+
41
+ setSharedMarketDataCoordinator(marketData);
42
+ setSharedNewsService(newsService);
43
+ const ready = Promise.all(plugins.map((plugin) => pluginRegistry.register(plugin))).then(() => {});
44
+ newsService.start();
45
+
46
+ return {
47
+ persistence,
48
+ tickerRepository,
49
+ dataProvider,
50
+ marketData,
51
+ pluginRegistry,
52
+ ready,
53
+ destroy() {
54
+ setSharedMarketDataCoordinator(null);
55
+ setSharedNewsService(null);
56
+ newsService.stop();
57
+ pluginRegistry.destroy();
58
+ persistence.close();
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,68 @@
1
+ import { afterEach, expect, spyOn, test } from "bun:test";
2
+ import { apiClient } from "../../api-client";
3
+ import {
4
+ browserCredentialedFetch,
5
+ restoreBrowserCloudSession,
6
+ } from "./cloud-transport";
7
+
8
+ const originalFetch = globalThis.fetch;
9
+ afterEach(() => { globalThis.fetch = originalFetch; });
10
+
11
+ test("browser cloud transport uses host cookies without forwarding forbidden headers", async () => {
12
+ let captured: RequestInit | undefined;
13
+ globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => {
14
+ captured = init;
15
+ return new Response("{}", { headers: { "content-type": "application/json" } });
16
+ }) as typeof fetch;
17
+ await browserCredentialedFetch("https://api.gloom.sh/auth/get-session", {
18
+ headers: { Cookie: "must-not-leak", Origin: "https://api.gloom.sh", Accept: "application/json" },
19
+ });
20
+ const headers = new Headers(captured?.headers);
21
+ expect(captured?.credentials).toBe("include");
22
+ expect(headers.has("Cookie")).toBe(false);
23
+ expect(headers.has("Origin")).toBe(false);
24
+ expect(headers.get("Accept")).toBe("application/json");
25
+ });
26
+
27
+ test("browser cloud transport plants session cookies before dropping the Cookie header", async () => {
28
+ const planted: string[] = [];
29
+ const previousDocument = (globalThis as { document?: unknown }).document;
30
+ (globalThis as { document?: { cookie: string } }).document = {
31
+ get cookie() {
32
+ return planted.join("; ");
33
+ },
34
+ set cookie(value: string) {
35
+ planted.push(value);
36
+ },
37
+ };
38
+ const previousLocation = (globalThis as { location?: unknown }).location;
39
+ (globalThis as { location?: { protocol: string } }).location = { protocol: "https:" };
40
+
41
+ globalThis.fetch = (async () => {
42
+ return new Response("{}", { headers: { "content-type": "application/json" } });
43
+ }) as typeof fetch;
44
+
45
+ try {
46
+ await browserCredentialedFetch("https://api.gloom.sh/auth/get-session", {
47
+ headers: {
48
+ Cookie: "__Secure-gloomberb.session_token=signed-token.value; gloomberb.session_token=signed-token.value",
49
+ },
50
+ });
51
+ expect(planted).toEqual([
52
+ "__Secure-gloomberb.session_token=signed-token.value; Path=/; SameSite=Lax; Secure",
53
+ "gloomberb.session_token=signed-token.value; Path=/; SameSite=Lax; Secure",
54
+ ]);
55
+ } finally {
56
+ if (previousDocument === undefined) delete (globalThis as { document?: unknown }).document;
57
+ else (globalThis as { document?: unknown }).document = previousDocument;
58
+ if (previousLocation === undefined) delete (globalThis as { location?: unknown }).location;
59
+ else (globalThis as { location?: unknown }).location = previousLocation;
60
+ }
61
+ });
62
+
63
+ test("browser boot restores an existing Gloom Cloud cookie session", async () => {
64
+ const getSession = spyOn(apiClient, "getSession").mockResolvedValue(null);
65
+ await restoreBrowserCloudSession();
66
+ expect(getSession).toHaveBeenCalledTimes(1);
67
+ getSession.mockRestore();
68
+ });
@@ -0,0 +1,48 @@
1
+ import { apiClient, setCloudApiFetchTransport } from "../../api-client";
2
+ import { setHttpFetchTransport } from "../../utils/http-transport";
3
+
4
+ const SESSION_COOKIE_NAMES = ["__Secure-gloomberb.session_token", "gloomberb.session_token"] as const;
5
+
6
+ function plantBrowserSessionCookies(cookieHeader: string): void {
7
+ if (typeof document === "undefined") return;
8
+ const secure = typeof location !== "undefined" && location.protocol === "https:";
9
+ for (const part of cookieHeader.split(";")) {
10
+ const trimmed = part.trim();
11
+ const eq = trimmed.indexOf("=");
12
+ if (eq <= 0) continue;
13
+ const name = trimmed.slice(0, eq).trim();
14
+ const value = trimmed.slice(eq + 1);
15
+ if (!(SESSION_COOKIE_NAMES as readonly string[]).includes(name) || !value) continue;
16
+ const useSecure = secure || name.startsWith("__Secure-");
17
+ document.cookie = `${name}=${value}; Path=/; SameSite=Lax${useSecure ? "; Secure" : ""}`;
18
+ }
19
+ }
20
+
21
+ export function browserCredentialedFetch(url: string, init: RequestInit = {}): Promise<Response> {
22
+ const headers = new Headers(init.headers);
23
+ const cookieHeader = headers.get("Cookie");
24
+ if (cookieHeader) plantBrowserSessionCookies(cookieHeader);
25
+ // These are controlled by the browser. Desktop transports may set them, but
26
+ // carrying them into fetch would either fail or misrepresent the web origin.
27
+ headers.delete("Cookie");
28
+ headers.delete("Origin");
29
+ return fetch(url, { ...init, headers, credentials: "include" });
30
+ }
31
+
32
+ export function installBrowserFetchTransports(): void {
33
+ apiClient.setCookieSessionMode(true);
34
+ setCloudApiFetchTransport(browserCredentialedFetch);
35
+ setHttpFetchTransport((url, init) => fetch(url, init));
36
+ }
37
+
38
+ export async function restoreBrowserCloudSession(budgetMs = 5_000): Promise<void> {
39
+ let timer: ReturnType<typeof setTimeout> | undefined;
40
+ const deadline = new Promise<null>((resolve) => {
41
+ timer = setTimeout(() => resolve(null), budgetMs);
42
+ });
43
+ try {
44
+ await Promise.race([apiClient.getSession().catch(() => null), deadline]);
45
+ } finally {
46
+ clearTimeout(timer);
47
+ }
48
+ }