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
@@ -93,6 +93,7 @@ beforeEach(() => {
93
93
  afterEach(() => {
94
94
  apiClient.dispose();
95
95
  apiClient.setSessionToken(null);
96
+ apiClient.setCookieSessionMode(false);
96
97
  apiClient.connectChannel = originalConnectChannel;
97
98
  apiClient.getSession = originalGetSession;
98
99
  apiClient.getMessages = originalGetMessages;
@@ -147,6 +148,47 @@ describe("ChatController", () => {
147
148
  expect(snapshot.messages.map((entry) => entry.id)).toEqual(["m1"]);
148
149
  });
149
150
 
151
+ test("uses a browser cookie session without exposing its token", async () => {
152
+ const persistence = new MemoryPersistence();
153
+ const controller = new ChatController();
154
+ const sentMessages: string[] = [];
155
+
156
+ apiClient.setCookieSessionMode(true);
157
+ apiClient.restoreCachedUser({
158
+ id: "u1",
159
+ username: "vince",
160
+ emailVerified: true,
161
+ });
162
+ apiClient.getSession = async () => apiClient.getCurrentUser();
163
+ apiClient.connectChannel = () => ({
164
+ send: async (content) => {
165
+ sentMessages.push(content);
166
+ return {
167
+ id: "m1",
168
+ channelId: "everyone",
169
+ content,
170
+ replyToId: null,
171
+ createdAt: "2026-08-23T00:00:00.000Z",
172
+ user: { id: "u1", username: "vince", displayName: "Vince" },
173
+ };
174
+ },
175
+ close: () => {},
176
+ });
177
+
178
+ controller.attachPersistence(persistence);
179
+ await controller.refreshSession();
180
+
181
+ expect(apiClient.getSessionToken()).toBeNull();
182
+ expect(controller.getSnapshot()).toMatchObject({
183
+ hasSavedSession: true,
184
+ user: { id: "u1", username: "vince", emailVerified: true },
185
+ });
186
+ expect(controller.send("hello from the browser")).toBe(true);
187
+ await flushMicrotasks();
188
+ expect(sentMessages).toEqual(["hello from the browser"]);
189
+ controller.dispose();
190
+ });
191
+
150
192
  test("rejects unknown shortcut channels after the server list loads", async () => {
151
193
  const controller = new ChatController();
152
194
  apiClient.getChannels = async () => SERVER_CHAT_CHANNELS;
@@ -157,6 +199,42 @@ describe("ChatController", () => {
157
199
  );
158
200
  });
159
201
 
202
+ test("keeps private channels when a public refresh finishes after chat state", async () => {
203
+ const persistence = new MemoryPersistence();
204
+ const controller = new ChatController();
205
+ const directChannel: ChatChannel = {
206
+ id: "dm:u2",
207
+ name: "u2",
208
+ kind: "direct",
209
+ created_at: "2026-03-28T00:00:00.000Z",
210
+ };
211
+ let resolvePublicChannels: ((channels: ChatChannel[]) => void) | undefined;
212
+
213
+ persistence.setState("session", {
214
+ sessionToken: "token-123",
215
+ user: { id: "u1", username: "vince", emailVerified: true },
216
+ }, { schemaVersion: 1 });
217
+ apiClient.getChannels = () => new Promise((resolve) => {
218
+ resolvePublicChannels = resolve;
219
+ });
220
+ apiClient.getChatState = async () => ({
221
+ channels: [...SERVER_CHAT_CHANNELS, directChannel],
222
+ onlineCount: 0,
223
+ channelStates: [],
224
+ notifications: [],
225
+ });
226
+ controller.attachPersistence(persistence);
227
+
228
+ const publicRefresh = controller.refreshChannels();
229
+ await controller.refreshChatState();
230
+ expect(controller.getChannels().map((channel) => channel.id)).toContain(directChannel.id);
231
+
232
+ resolvePublicChannels!(SERVER_CHAT_CHANNELS);
233
+ await publicRefresh;
234
+
235
+ expect(controller.getChannels().map((channel) => channel.id)).toContain(directChannel.id);
236
+ });
237
+
160
238
  test("hydrates a cached verified user into the api client for offline use", async () => {
161
239
  const persistence = new MemoryPersistence();
162
240
  const controller = new ChatController();
@@ -375,6 +453,55 @@ describe("ChatController", () => {
375
453
  }
376
454
  });
377
455
 
456
+ test("keeps a persisted native session when get-session returns no user", async () => {
457
+ const persistence = new MemoryPersistence();
458
+ const controller = new ChatController();
459
+
460
+ persistence.setState("session", {
461
+ sessionToken: "token-123",
462
+ user: { id: "u1", username: "vince", emailVerified: true },
463
+ }, { schemaVersion: 1 });
464
+
465
+ controller.attachPersistence(persistence);
466
+ apiClient.getSession = async () => null;
467
+
468
+ await controller.refreshSession();
469
+
470
+ expect(apiClient.getSessionToken()).toBe("token-123");
471
+ expect(persistence.getState<{
472
+ sessionToken: string;
473
+ user: { id: string; username: string; emailVerified: boolean };
474
+ }>("session", { schemaVersion: 1 })).toEqual({
475
+ sessionToken: "token-123",
476
+ user: { id: "u1", username: "vince", emailVerified: true },
477
+ });
478
+ expect(controller.getSnapshot().user).toEqual({
479
+ id: "u1",
480
+ username: "vince",
481
+ emailVerified: true,
482
+ });
483
+ controller.dispose();
484
+ });
485
+
486
+ test("signs out when get-session returns no user and no token is stored", async () => {
487
+ const persistence = new MemoryPersistence();
488
+ const controller = new ChatController();
489
+
490
+ persistence.setState("session", {
491
+ sessionToken: null,
492
+ user: { id: "u1", username: "vince", emailVerified: true },
493
+ }, { schemaVersion: 1 });
494
+
495
+ controller.attachPersistence(persistence);
496
+ apiClient.getSession = async () => null;
497
+
498
+ await controller.refreshSession();
499
+
500
+ expect(apiClient.getSessionToken()).toBeNull();
501
+ expect(controller.getSnapshot().user).toBeNull();
502
+ controller.dispose();
503
+ });
504
+
378
505
  test("keeps the cached session when session refresh fails transiently", async () => {
379
506
  const persistence = new MemoryPersistence();
380
507
  const controller = new ChatController();
@@ -1021,8 +1148,7 @@ describe("ChatController", () => {
1021
1148
  (controller as any).handleChatNotification(mentionNotification());
1022
1149
 
1023
1150
  expect(notifications).toEqual([{
1024
- title: "Gloomberb chat",
1025
- subtitle: "#everyone",
1151
+ title: "#everyone",
1026
1152
  body: "@bob mentioned you: hey @vince",
1027
1153
  type: "info",
1028
1154
  desktop: "when-inactive",
@@ -1246,8 +1372,7 @@ describe("ChatController", () => {
1246
1372
  unreadCount: 3,
1247
1373
  });
1248
1374
  expect(notifications).toEqual([{
1249
- title: "Gloomberb chat",
1250
- subtitle: "#options",
1375
+ title: "#options",
1251
1376
  body: "@bob replied to you: answering you",
1252
1377
  type: "info",
1253
1378
  desktop: "when-inactive",
@@ -1327,10 +1452,11 @@ describe("ChatController", () => {
1327
1452
  expect(notifications).toHaveLength(1);
1328
1453
  });
1329
1454
 
1330
- test("displays server-issued channel notifications", () => {
1455
+ test("opens a server-issued channel notification at its exact message", () => {
1331
1456
  const persistence = new MemoryPersistence();
1332
1457
  const controller = new ChatController();
1333
1458
  const notifications: AppNotificationRequest[] = [];
1459
+ const openedMessages: string[] = [];
1334
1460
  const message: ChatMessage = {
1335
1461
  id: "m1",
1336
1462
  channelId: "options",
@@ -1346,6 +1472,8 @@ describe("ChatController", () => {
1346
1472
  }, { schemaVersion: 1 });
1347
1473
  controller.setNotifier((entry) => {
1348
1474
  notifications.push(entry);
1475
+ }, (channelId, messageId) => {
1476
+ openedMessages.push(`${channelId}:${messageId}`);
1349
1477
  });
1350
1478
  controller.attachPersistence(persistence);
1351
1479
 
@@ -1359,15 +1487,17 @@ describe("ChatController", () => {
1359
1487
  } satisfies ChatNotification);
1360
1488
 
1361
1489
  expect(notifications).toEqual([{
1362
- title: "Gloomberb chat",
1363
- subtitle: "#options",
1364
- body: "#options @bob: new option flow",
1490
+ title: "#options",
1491
+ body: "@bob: new option flow",
1365
1492
  type: "info",
1366
1493
  desktop: "when-inactive",
1494
+ action: expect.objectContaining({ label: "Open" }),
1367
1495
  }]);
1496
+ notifications[0]?.action?.onClick();
1497
+ expect(openedMessages).toEqual(["options:m1"]);
1368
1498
  });
1369
1499
 
1370
- test("uses direct channel labels in server-issued notification subtitles", async () => {
1500
+ test("uses direct channel labels in server-issued notification titles", async () => {
1371
1501
  const persistence = new MemoryPersistence();
1372
1502
  const controller = new ChatController();
1373
1503
  const notifications: AppNotificationRequest[] = [];
@@ -1418,9 +1548,8 @@ describe("ChatController", () => {
1418
1548
  } satisfies ChatNotification);
1419
1549
 
1420
1550
  expect(notifications).toEqual([{
1421
- title: "Gloomberb chat",
1422
- subtitle: "@bob",
1423
- body: "@bob: ping",
1551
+ title: "@bob",
1552
+ body: "ping",
1424
1553
  type: "info",
1425
1554
  desktop: "when-inactive",
1426
1555
  }]);
@@ -1491,8 +1620,7 @@ describe("ChatController", () => {
1491
1620
  } satisfies ChatNotification);
1492
1621
 
1493
1622
  expect(notifications).toEqual([{
1494
- title: "Gloomberb chat",
1495
- subtitle: "#options",
1623
+ title: "#options",
1496
1624
  body: "@bob replied to you: reply without channel notify",
1497
1625
  type: "info",
1498
1626
  desktop: "when-inactive",
@@ -0,0 +1,88 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { createTestRenderer } from "@opentui/core/testing";
3
+ import { act } from "react";
4
+ import { createOpenTuiTestRoot as createRoot } from "../../../../renderers/opentui/test-utils";
5
+ import { getThemeColors, syncTheme } from "../../../../theme/colors";
6
+ import { ThemeProvider } from "../../../../theme/theme-context";
7
+ import { DEFAULT_THEME } from "../../../../theme/themes";
8
+ import type { ChatMessage } from "../../../../api-client";
9
+ import { DesktopChatMessage } from "./desktop";
10
+
11
+ let testSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined;
12
+ let root: ReturnType<typeof createRoot> | undefined;
13
+ const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean };
14
+ actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
15
+
16
+ function rgba(hex: string): string {
17
+ const value = hex.slice(1);
18
+ return [0, 2, 4].map((offset) => parseInt(value.slice(offset, offset + 2), 16)).concat(255).join(",");
19
+ }
20
+
21
+ afterEach(async () => {
22
+ if (root) {
23
+ await act(async () => {
24
+ root?.unmount();
25
+ await Promise.resolve();
26
+ });
27
+ }
28
+ testSetup?.renderer.destroy();
29
+ root = undefined;
30
+ testSetup = undefined;
31
+ syncTheme(DEFAULT_THEME);
32
+ });
33
+
34
+ describe("DesktopChatMessage", () => {
35
+ test("updates memoized row colors when the theme changes", async () => {
36
+ const message: ChatMessage = {
37
+ id: "theme-message",
38
+ channelId: "everyone",
39
+ content: "theme message",
40
+ replyToId: null,
41
+ createdAt: "2026-08-25T12:00:00.000Z",
42
+ user: { id: "user-1", username: "vince", displayName: "Vince" },
43
+ };
44
+ const noop = () => {};
45
+ const messageElement = (
46
+ <DesktopChatMessage
47
+ msg={message}
48
+ index={0}
49
+ messages={[message]}
50
+ selectedIdx={0}
51
+ hoveredIdx={null}
52
+ canSend={false}
53
+ catalog={{}}
54
+ userByUsername={new Map()}
55
+ openTicker={noop}
56
+ onUserHover={noop}
57
+ onUserHoverEnd={noop}
58
+ beginReplyTo={noop}
59
+ beginEditMessage={() => false}
60
+ jumpToMessage={noop}
61
+ latestEditableMessageId={null}
62
+ registerMessageElement={noop}
63
+ />
64
+ );
65
+
66
+ testSetup = await createTestRenderer({ width: 60, height: 4 });
67
+ root = createRoot(testSetup.renderer);
68
+ act(() => root?.render(<ThemeProvider themeId={DEFAULT_THEME}>{messageElement}</ThemeProvider>));
69
+ await act(async () => {
70
+ await testSetup?.renderOnce();
71
+ await testSetup?.renderOnce();
72
+ });
73
+
74
+ const nextThemeId = "github-light";
75
+ act(() => root?.render(<ThemeProvider themeId={nextThemeId}>{messageElement}</ThemeProvider>));
76
+ await act(async () => {
77
+ await testSetup?.renderOnce();
78
+ await testSetup?.renderOnce();
79
+ });
80
+
81
+ const bodySpan = testSetup.captureSpans().lines
82
+ .flatMap((line) => line.spans)
83
+ .find((span) => span.text.includes(message.content));
84
+ const palette = getThemeColors(nextThemeId);
85
+ expect(bodySpan?.fg.toInts().join(",")).toBe(rgba(palette.selectedText));
86
+ expect(bodySpan?.bg.toInts().join(",")).toBe(rgba(palette.selected));
87
+ });
88
+ });
@@ -1,6 +1,7 @@
1
1
  import { memo } from "react";
2
2
  import { Box, Span, Text } from "../../../../ui";
3
3
  import { hoverBg } from "../../../../theme/colors";
4
+ import { useThemeColors } from "../../../../theme/theme-context";
4
5
  import { t } from "../../../../i18n";
5
6
  import { useAppLanguage } from "../../../../i18n/react";
6
7
  import { MESSAGE_ACTION_WIDTH, normalizeInlinePreview } from "../layout";
@@ -32,6 +33,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
32
33
  registerMessageElement: (messageId: string, node: unknown | null) => void;
33
34
  }) {
34
35
  useAppLanguage();
36
+ const themeColors = useThemeColors();
35
37
  const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
36
38
  const canEditMessage = msg.id === latestEditableMessageId;
37
39
  const showInlineReplyAction = !state.grouped && canSend;
@@ -58,7 +60,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
58
60
  data-gloom-role="chat-message"
59
61
  data-gloom-chat-message-id={msg.id}
60
62
  data-selected={state.isSelected ? "true" : "false"}
61
- style={{ "--chat-hover-bg": hoverBg(), minWidth: 0 }}
63
+ style={{ "--chat-hover-bg": hoverBg(themeColors), minWidth: 0 }}
62
64
  >
63
65
  {msg.replyTo && (
64
66
  <Box
@@ -2,11 +2,11 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useEffect } from "react";
3
3
  import {
4
4
  apiClient,
5
- type AccountProfile,
6
5
  type ChatUserSummary,
7
6
  } from "../../../../api-client";
8
7
  import { testRender } from "../../../../renderers/opentui/test-utils";
9
8
  import { Box, Text } from "../../../../ui";
9
+ import { makeAccountProfile } from "../test-harness";
10
10
  import { useChatProfilePopover } from "../profile-popover";
11
11
  import {
12
12
  hasPublicChatProfileInfo,
@@ -36,34 +36,6 @@ function makeUser(overrides: Partial<ChatUserSummary>): ChatUserSummary {
36
36
  };
37
37
  }
38
38
 
39
- function makeAccountProfile(overrides: Partial<AccountProfile> = {}): AccountProfile {
40
- return {
41
- id: "u1",
42
- email: "vince@example.com",
43
- emailVerified: true,
44
- plan: "pro",
45
- username: "vince",
46
- name: "Vince",
47
- company: "Gloom",
48
- title: null,
49
- bio: "Made Gloomberb",
50
- profilePublic: true,
51
- publicEmail: null,
52
- xAccount: null,
53
- sharedPortfolioId: "broker:ibkr:coldstart",
54
- acceptUnknownDms: false,
55
- chatEmailNotificationsEnabled: true,
56
- portfolioAnalytics: null,
57
- syncEnabled: true,
58
- weeklyRoundupEnabled: true,
59
- positionAlertsEnabled: true,
60
- lastSyncAt: null,
61
- lastRoundupEmailAt: null,
62
- updatedAt: "2026-07-21T22:03:59.832Z",
63
- ...overrides,
64
- };
65
- }
66
-
67
39
  function OwnProfileHarness({ user }: { user: ChatUserSummary }) {
68
40
  const {
69
41
  profilePopoverUser,
@@ -23,8 +23,23 @@ function accountProfileToChatUser(profile: AccountProfile): ChatUserSummary {
23
23
  };
24
24
  }
25
25
 
26
- export function useChatProfilePopover() {
26
+ const OPTIONAL_PROFILE_FIELDS = [
27
+ "company",
28
+ "title",
29
+ "bio",
30
+ "publicEmail",
31
+ "xAccount",
32
+ "sharedPortfolioId",
33
+ ] as const;
34
+
35
+ /** `profilePublic` is a visibility switch, not a completion test: any filled optional field counts as set up. */
36
+ export function isAccountProfileConfigured(profile: AccountProfile): boolean {
37
+ return OPTIONAL_PROFILE_FIELDS.some((field) => (profile[field] ?? "").trim().length > 0);
38
+ }
39
+
40
+ export function useChatProfilePopover(trackOwnProfileUserId?: string) {
27
41
  const [profilePopoverUser, setProfilePopoverUser] = useState<ChatUserSummary | null>(null);
42
+ const [ownProfileConfigured, setOwnProfileConfigured] = useState<boolean | null>(null);
28
43
  const profilePopoverCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
29
44
  const ownProfileRef = useRef<ChatUserSummary | null>(null);
30
45
  const ownProfileRequestRef = useRef<Promise<void> | null>(null);
@@ -50,34 +65,25 @@ export function useChatProfilePopover() {
50
65
  }, PROFILE_POPOVER_CLOSE_DELAY_MS);
51
66
  }, [cancelProfilePopoverClose]);
52
67
 
53
- const showProfilePopover = useCallback((
54
- targetUser: ChatUserSummary,
55
- options?: { ownProfile?: boolean },
56
- ) => {
57
- const ownProfile = options?.ownProfile === true;
58
- const cachedUser = ownProfile && ownProfileRef.current?.id === targetUser.id
59
- ? ownProfileRef.current
60
- : targetUser;
61
- if (!ownProfile && !hasPublicChatProfileInfo(cachedUser)) {
62
- closeProfilePopover();
63
- return;
64
- }
65
- cancelProfilePopoverClose();
66
- setProfilePopoverUser(cachedUser);
67
-
68
+ const refreshOwnProfile = useCallback((expectedUserId?: string, force = false) => {
68
69
  if (
69
- !ownProfile
70
- || !apiClient.getSessionToken()
70
+ !apiClient.isSignedIn()
71
71
  || ownProfileRequestRef.current
72
- || (ownProfileRef.current?.id === targetUser.id && Date.now() - ownProfileLoadedAtRef.current < 10_000)
72
+ || (
73
+ !force
74
+ && ownProfileRef.current
75
+ && (!expectedUserId || ownProfileRef.current.id === expectedUserId)
76
+ && Date.now() - ownProfileLoadedAtRef.current < 10_000
77
+ )
73
78
  ) return;
74
79
  const request = apiClient.getAccountProfile()
75
80
  .then((profile) => {
76
- if (!activeRef.current || profile.id !== targetUser.id) return;
81
+ if (!activeRef.current || (expectedUserId && profile.id !== expectedUserId)) return;
77
82
  const nextUser = accountProfileToChatUser(profile);
78
83
  ownProfileRef.current = nextUser;
79
84
  ownProfileLoadedAtRef.current = Date.now();
80
- setProfilePopoverUser((current) => current?.id === targetUser.id ? nextUser : current);
85
+ setOwnProfileConfigured(isAccountProfileConfigured(profile));
86
+ setProfilePopoverUser((current) => current?.id === profile.id ? nextUser : current);
81
87
  })
82
88
  .catch(() => {})
83
89
  .finally(() => {
@@ -86,7 +92,24 @@ export function useChatProfilePopover() {
86
92
  }
87
93
  });
88
94
  ownProfileRequestRef.current = request;
89
- }, [cancelProfilePopoverClose, closeProfilePopover]);
95
+ }, []);
96
+
97
+ const showProfilePopover = useCallback((
98
+ targetUser: ChatUserSummary,
99
+ options?: { ownProfile?: boolean },
100
+ ) => {
101
+ const ownProfile = options?.ownProfile === true;
102
+ const cachedUser = ownProfile && ownProfileRef.current?.id === targetUser.id
103
+ ? ownProfileRef.current
104
+ : targetUser;
105
+ if (!ownProfile && !hasPublicChatProfileInfo(cachedUser)) {
106
+ closeProfilePopover();
107
+ return;
108
+ }
109
+ cancelProfilePopoverClose();
110
+ setProfilePopoverUser(cachedUser);
111
+ if (ownProfile) refreshOwnProfile(targetUser.id);
112
+ }, [cancelProfilePopoverClose, closeProfilePopover, refreshOwnProfile]);
90
113
 
91
114
  useEffect(() => {
92
115
  activeRef.current = true;
@@ -96,9 +119,17 @@ export function useChatProfilePopover() {
96
119
  };
97
120
  }, [cancelProfilePopoverClose]);
98
121
 
122
+ // Force a refresh when the pane regains focus so returning from Account Management settles the answer.
123
+ useEffect(() => {
124
+ if (!trackOwnProfileUserId) return;
125
+ if (ownProfileRef.current?.id !== trackOwnProfileUserId) setOwnProfileConfigured(null);
126
+ refreshOwnProfile(trackOwnProfileUserId, true);
127
+ }, [refreshOwnProfile, trackOwnProfileUserId]);
128
+
99
129
  return {
100
130
  cancelProfilePopoverClose,
101
131
  closeProfilePopover,
132
+ ownProfileConfigured,
102
133
  profilePopoverUser,
103
134
  scheduleProfilePopoverClose,
104
135
  showProfilePopover,
@@ -11,6 +11,10 @@ import { gloomberbCloudPlugin } from "../cloud";
11
11
  import { ChatContent } from "./content";
12
12
  import { chatController } from "./controller";
13
13
  import { useChatChannelNavigation } from "./content/channel-navigation";
14
+ import {
15
+ subscribeRequestedAccountManagementTab,
16
+ type AccountManagementTab,
17
+ } from "../account-management/navigation";
14
18
  import {
15
19
  cleanupChatTest,
16
20
  createChatTestControls,
@@ -19,6 +23,7 @@ import {
19
23
  installChatApiTestDefaults,
20
24
  installServerChannels,
21
25
  lineText,
26
+ makeAccountProfile,
22
27
  makeMessage,
23
28
  MemoryPersistence,
24
29
  type ChatTestSetup,
@@ -312,6 +317,82 @@ describe("ChatContent channel sidebar", () => {
312
317
  expect(setup().captureCharFrame()).toContain("● 6 online");
313
318
  });
314
319
 
320
+ test("offers a sidebar profile shortcut only until the account profile is filled in", async () => {
321
+ const controller = createController({ sessionToken: "token-123" });
322
+ installServerChannels(controller);
323
+ controller.refreshChannels = async () => {};
324
+ controller.refreshChannelMessages = async () => {};
325
+ const originalGetAccountProfile = apiClient.getAccountProfile.bind(apiClient);
326
+ apiClient.getAccountProfile = async () => makeAccountProfile({
327
+ id: "u0",
328
+ company: null,
329
+ title: null,
330
+ bio: null,
331
+ publicEmail: null,
332
+ xAccount: null,
333
+ // Set, and deliberately not part of the completion test.
334
+ profilePublic: true,
335
+ sharedPortfolioId: null,
336
+ });
337
+ const requestedTabs: AccountManagementTab[] = [];
338
+ const unsubscribe = subscribeRequestedAccountManagementTab((tab) => requestedTabs.push(tab));
339
+ const shownPanes: string[] = [];
340
+ const runtime = createTestPluginRuntime({
341
+ showPane: (paneId: string) => {
342
+ shownPanes.push(paneId);
343
+ },
344
+ });
345
+
346
+ try {
347
+ await act(async () => {
348
+ testSetup = await testRender(createHarness(controller, { width: 90, height: 14, runtime }), {
349
+ width: 90,
350
+ height: 14,
351
+ });
352
+ });
353
+ await flushFrame();
354
+ await flushFrame();
355
+
356
+ const lines = setup().captureCharFrame().split("\n");
357
+ const row = lines.findIndex((line) => line.includes("@ Profile"));
358
+ const col = lines[row]?.indexOf("Profile") ?? -1;
359
+ expect(row).toBeGreaterThanOrEqual(0);
360
+
361
+ await act(async () => {
362
+ await setup().mockMouse.click(col + 1, row);
363
+ await setup().renderOnce();
364
+ await setup().renderOnce();
365
+ });
366
+ await flushFrame();
367
+
368
+ expect(requestedTabs).toEqual(["profile"]);
369
+ expect(shownPanes).toEqual(["account-management"]);
370
+
371
+ apiClient.getAccountProfile = async () => makeAccountProfile({
372
+ id: "u0",
373
+ company: null,
374
+ title: "Trader",
375
+ bio: null,
376
+ profilePublic: false,
377
+ sharedPortfolioId: null,
378
+ });
379
+ await act(async () => {
380
+ testSetup?.renderer.destroy();
381
+ testSetup = await testRender(createHarness(controller, { width: 90, height: 14, runtime }), {
382
+ width: 90,
383
+ height: 14,
384
+ });
385
+ });
386
+ await flushFrame();
387
+ await flushFrame();
388
+
389
+ expect(setup().captureCharFrame()).not.toContain("@ Profile");
390
+ } finally {
391
+ unsubscribe();
392
+ apiClient.getAccountProfile = originalGetAccountProfile;
393
+ }
394
+ });
395
+
315
396
  test("uses arrows to move between channel sidebar and chat content", async () => {
316
397
  const controller = createController({ sessionToken: "token-123" });
317
398
  installServerChannels(controller);