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
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { Box, Text, useUiHost } from "../../../ui";
2
+ import { Box, Text, useUiCapabilities, useUiHost } from "../../../ui";
3
3
  import {
4
4
  ChoiceDialog,
5
5
  Tabs,
@@ -32,7 +32,9 @@ import { SeriesEditorDialog } from "./editor";
32
32
  import { chartComposerSemanticMetadata } from "./semantic";
33
33
  import {
34
34
  canToggleChartSeries,
35
+ CHART_INTERACTION_VIEWPORT_SETTING_KEY,
35
36
  CHART_SPEC_SETTING_KEY,
37
+ parseChartInteractionViewport,
36
38
  parseChartSpecOr,
37
39
  projectVisibleChartSeries,
38
40
  toggleChartSeries,
@@ -50,6 +52,7 @@ import {
50
52
  type BuiltinStudySelection,
51
53
  type PairStudySelection,
52
54
  } from "./presets";
55
+ import type { ChartInteractionViewport } from "./chart-spec";
53
56
  import {
54
57
  CHART_FORMULA_OPTIONS,
55
58
  CHART_RANGES as RANGES,
@@ -59,6 +62,9 @@ import {
59
62
  import { resolveChartComposerShortcut } from "./shortcuts";
60
63
  import { ChartSeriesQuickAdd } from "./quick-add";
61
64
  import { useLiveStreamingSetting } from "../shared/live-streaming";
65
+ import { usePublicShare } from "../shared/public-share";
66
+ import { buildChartShareData } from "../../../shares/chart-snapshot";
67
+ import { isPlainKey } from "../../../utils/keyboard";
62
68
 
63
69
  const RANGE_TABS = RANGES.map((range, index) => ({ label: `${index + 1}:${range}`, value: range }));
64
70
  const AUTO_VIEWPORT_DEBOUNCE_MS = 350;
@@ -68,6 +74,28 @@ interface RuntimeChartViewport {
68
74
  end: Date;
69
75
  }
70
76
 
77
+ interface RuntimeChartViewportState {
78
+ key: string;
79
+ adaptiveViewport: RuntimeChartViewport | null;
80
+ requestViewport: RuntimeChartViewport;
81
+ }
82
+
83
+ function runtimeViewportFromSetting(
84
+ setting: ChartInteractionViewport | null,
85
+ authoredViewportKey: string,
86
+ ): RuntimeChartViewportState | null {
87
+ if (!setting || setting.authoredViewportKey !== authoredViewportKey) return null;
88
+ const requestViewport = {
89
+ start: new Date(setting.start),
90
+ end: new Date(setting.end),
91
+ };
92
+ return {
93
+ key: authoredViewportKey,
94
+ adaptiveViewport: setting.adaptive ? requestViewport : null,
95
+ requestViewport,
96
+ };
97
+ }
98
+
71
99
  function footerAnchorPoint(event?: PaneFooterPressEvent): { x: number; y: number } | undefined {
72
100
  const x = event?.pixelX;
73
101
  const y = event?.pixelY;
@@ -84,6 +112,7 @@ interface ChartComposerSurfaceProps {
84
112
  height: number;
85
113
  footerId: string;
86
114
  onCapture?: (capturing: boolean) => void;
115
+ liveWhenUnfocused?: boolean;
87
116
  }
88
117
 
89
118
  const QUICK_ADD_CAPTURE = "quick-add";
@@ -103,10 +132,12 @@ function ChartComposerSurface({
103
132
  height,
104
133
  footerId,
105
134
  onCapture,
135
+ liveWhenUnfocused = true,
106
136
  }: ChartComposerSurfaceProps) {
107
137
  const dialog = useDialog();
108
138
  const dispatch = useAppDispatch();
109
139
  const isDesktopWeb = useUiHost().kind === "desktop-web";
140
+ const { publicSharing } = useUiCapabilities();
110
141
  const paneId = usePaneInstanceId();
111
142
  const liveStreaming = useLiveStreamingSetting();
112
143
  const dialogOpen = useDialogState((state) => state.isOpen);
@@ -131,11 +162,17 @@ function ChartComposerSurface({
131
162
  ? [entry.id, entry.source.kind, entry.source.seriesId]
132
163
  : [entry.id, entry.source.kind, chartSeriesSourceKey(entry.source)]),
133
164
  }), [spec.series, spec.viewport.dateWindow, spec.viewport.maxPoints, spec.viewport.range, spec.viewport.resolution]);
134
- const [runtimeViewportState, setRuntimeViewportState] = useState<{
135
- key: string;
136
- adaptiveViewport: RuntimeChartViewport | null;
137
- requestViewport: RuntimeChartViewport;
138
- } | null>(null);
165
+ const [storedInteractionViewport, setStoredInteractionViewport] = usePaneSettingValue<unknown>(
166
+ CHART_INTERACTION_VIEWPORT_SETTING_KEY,
167
+ null,
168
+ );
169
+ const persistedInteractionViewport = useMemo(
170
+ () => parseChartInteractionViewport(storedInteractionViewport),
171
+ [storedInteractionViewport],
172
+ );
173
+ const [runtimeViewportState, setRuntimeViewportState] = useState<RuntimeChartViewportState | null>(() => (
174
+ runtimeViewportFromSetting(persistedInteractionViewport, authoredViewportKey)
175
+ ));
139
176
  const runtimeViewportTimerRef = useRef<ReturnType<typeof globalThis.setTimeout> | null>(null);
140
177
  const adaptiveViewportRef = useRef<RuntimeChartViewport | null>(null);
141
178
  const requestViewportRef = useRef<RuntimeChartViewport | null>(null);
@@ -149,7 +186,7 @@ function ChartComposerSurface({
149
186
  : null,
150
187
  requestViewport: activeRuntimeViewport?.requestViewport,
151
188
  targetPointCount,
152
- liveStreaming,
189
+ liveStreaming: liveStreaming && (liveWhenUnfocused || focused),
153
190
  });
154
191
  const availableResolutions = useMemo<ChartResolution[]>(() => {
155
192
  if (!resolution.resolutionSupport) {
@@ -220,6 +257,11 @@ function ChartComposerSurface({
220
257
  ),
221
258
  [resolution.bufferedSeries, resolution.legendSeries, resolution.series, spec],
222
259
  );
260
+ const shareData = useMemo(() => buildChartShareData(plottedSeries), [plottedSeries]);
261
+ const createPublicShare = usePublicShare();
262
+ const shareChart = useCallback(() => {
263
+ if (shareData) void createPublicShare({ kind: "chart", data: shareData });
264
+ }, [createPublicShare, shareData]);
223
265
  const [interactionCaptured, setInteractionCapturedState] = useState(false);
224
266
  // Typing in quick-add must not freeze the plot: it only takes the keyboard.
225
267
  const [modalCaptured, setModalCaptured] = useState(false);
@@ -261,16 +303,21 @@ function ChartComposerSurface({
261
303
  clearTimeout(runtimeViewportTimerRef.current);
262
304
  runtimeViewportTimerRef.current = null;
263
305
  }
264
- adaptiveViewportRef.current = null;
265
- requestViewportRef.current = null;
266
- setRuntimeViewportState((current) => current?.key === authoredViewportKey ? current : null);
306
+ const restored = runtimeViewportFromSetting(
307
+ persistedInteractionViewport,
308
+ authoredViewportKey,
309
+ );
310
+ adaptiveViewportRef.current = restored?.adaptiveViewport ?? null;
311
+ requestViewportRef.current = restored?.requestViewport ?? null;
312
+ setRuntimeViewportState((current) => current?.key === authoredViewportKey ? current : restored);
313
+ if (persistedInteractionViewport && !restored) setStoredInteractionViewport(null);
267
314
  return () => {
268
315
  if (runtimeViewportTimerRef.current !== null) {
269
316
  clearTimeout(runtimeViewportTimerRef.current);
270
317
  runtimeViewportTimerRef.current = null;
271
318
  }
272
319
  };
273
- }, [authoredViewportKey]);
320
+ }, [authoredViewportKey, persistedInteractionViewport, setStoredInteractionViewport]);
274
321
  const handleChartViewportChange = useCallback((
275
322
  next: { start: Date; end: Date } | null,
276
323
  interaction: "pan" | "reset" | "sync" | "zoom",
@@ -284,6 +331,7 @@ function ChartComposerSurface({
284
331
  adaptiveViewportRef.current = null;
285
332
  requestViewportRef.current = null;
286
333
  setRuntimeViewportState(null);
334
+ setStoredInteractionViewport(null);
287
335
  return;
288
336
  }
289
337
  const start = next.start.getTime();
@@ -306,8 +354,14 @@ function ChartComposerSurface({
306
354
  adaptiveViewport,
307
355
  requestViewport,
308
356
  });
357
+ setStoredInteractionViewport({
358
+ authoredViewportKey,
359
+ start: requestViewport.start.toISOString(),
360
+ end: requestViewport.end.toISOString(),
361
+ adaptive: adaptiveViewport !== null,
362
+ } satisfies ChartInteractionViewport);
309
363
  }, AUTO_VIEWPORT_DEBOUNCE_MS);
310
- }, [authoredViewportKey, spec.viewport.resolution]);
364
+ }, [authoredViewportKey, setStoredInteractionViewport, spec.viewport.resolution]);
311
365
 
312
366
  useRemoteUiNode({
313
367
  role: "chart-data",
@@ -444,6 +498,12 @@ function ChartComposerSurface({
444
498
 
445
499
  useShortcut((event) => {
446
500
  if (interactionCaptureRef.current || dialogOpen) return;
501
+ if (publicSharing && shareData && isPlainKey(event, "y")) {
502
+ event.preventDefault();
503
+ event.stopPropagation();
504
+ shareChart();
505
+ return;
506
+ }
447
507
  const shortcut = resolveChartComposerShortcut(event, RANGES.length);
448
508
  if (!shortcut) return;
449
509
  event.preventDefault();
@@ -479,6 +539,9 @@ function ChartComposerSurface({
479
539
  { id: "formulas", key: "f", label: "ormulas", onPress: openFormulas, disabled: formulasDisabled },
480
540
  { id: "resolution", key: "t", label: "imeframe", onPress: footerResolution },
481
541
  { id: "range", key: "1-8", label: "range", onPress: footerRange },
542
+ ...(publicSharing
543
+ ? [{ id: "share", key: "y", label: " share", onPress: shareChart, disabled: !shareData }]
544
+ : []),
482
545
  ],
483
546
  }), [
484
547
  footerRange,
@@ -489,6 +552,9 @@ function ChartComposerSurface({
489
552
  indicatorsDisabled,
490
553
  openFormulas,
491
554
  openIndicators,
555
+ publicSharing,
556
+ shareChart,
557
+ shareData,
492
558
  resolution.errors,
493
559
  resolution.loading,
494
560
  resolution.warnings,
@@ -635,6 +701,20 @@ export function ChartComposerPane({ paneId, focused, width, height }: PaneProps)
635
701
  );
636
702
  }
637
703
 
704
+ function firstChartSecuritySymbol(spec: ChartSpec): string | null {
705
+ for (const entry of spec.series) {
706
+ if (entry.source.kind === "security") return entry.source.instrument.symbol;
707
+ }
708
+ return null;
709
+ }
710
+
711
+ function specHasSecuritySymbol(spec: ChartSpec, symbol: string | null | undefined): boolean {
712
+ if (!symbol) return false;
713
+ return spec.series.some((entry) => (
714
+ entry.source.kind === "security" && entry.source.instrument.symbol === symbol
715
+ ));
716
+ }
717
+
638
718
  export function ChartComposerResearchTab({ focused, width, height, onCapture }: TickerResearchTabProps) {
639
719
  const { symbol } = usePaneTicker();
640
720
  const fallback = useMemo(() => symbol ? buildPriceChartPreset(symbol) : buildEmptyChartPreset(), [symbol]);
@@ -643,11 +723,18 @@ export function ChartComposerResearchTab({ focused, width, height, onCapture }:
643
723
  const previousSymbolRef = useRef(symbol);
644
724
 
645
725
  useEffect(() => {
726
+ if (!symbol) return;
646
727
  const previousSymbol = previousSymbolRef.current;
647
- previousSymbolRef.current = symbol;
648
- if (!symbol || !previousSymbol || symbol === previousSymbol) return;
649
- const rebound = rebindChartSecuritySymbol(spec, previousSymbol, symbol);
728
+ const fromSymbol = specHasSecuritySymbol(spec, previousSymbol)
729
+ ? previousSymbol
730
+ : firstChartSecuritySymbol(spec) ?? previousSymbol;
731
+ if (!fromSymbol || fromSymbol === symbol) {
732
+ previousSymbolRef.current = symbol;
733
+ return;
734
+ }
735
+ const rebound = rebindChartSecuritySymbol(spec, fromSymbol, symbol);
650
736
  if (rebound !== spec) setStoredSpec(rebound);
737
+ previousSymbolRef.current = symbol;
651
738
  }, [setStoredSpec, spec, symbol]);
652
739
 
653
740
  return (
@@ -659,6 +746,7 @@ export function ChartComposerResearchTab({ focused, width, height, onCapture }:
659
746
  height={height}
660
747
  footerId="chart-composer:research"
661
748
  onCapture={onCapture}
749
+ liveWhenUnfocused={false}
662
750
  />
663
751
  );
664
752
  }
@@ -240,10 +240,11 @@ export function ChatContent({
240
240
  const {
241
241
  cancelProfilePopoverClose,
242
242
  closeProfilePopover,
243
+ ownProfileConfigured,
243
244
  profilePopoverUser,
244
245
  scheduleProfilePopoverClose,
245
246
  showProfilePopover,
246
- } = useChatProfilePopover();
247
+ } = useChatProfilePopover(focused ? user?.id : undefined);
247
248
 
248
249
  const showUserProfilePopover = useCallback((targetUser: Parameters<typeof showProfilePopover>[0]) => {
249
250
  showProfilePopover(targetUser, { ownProfile: targetUser.id === user?.id });
@@ -537,6 +538,8 @@ export function ChatContent({
537
538
  loading={channelsLoading}
538
539
  canManageNotifications={!!user?.emailVerified}
539
540
  canCreateConversation={!!user?.emailVerified}
541
+ needsProfileSetup={!!user?.id && ownProfileConfigured === false}
542
+ onOpenProfile={openProfileSetup}
540
543
  directExpanded={directExpanded}
541
544
  onSelect={selectSidebarChannel}
542
545
  onFocusRequest={() => setSidebarFocused(true)}
@@ -68,7 +68,13 @@ export class ChatControllerChannels {
68
68
 
69
69
  const request = apiClient.getChannels()
70
70
  .then((channels) => {
71
- this.channels = normalizeChannels(channels);
71
+ const publicChannels = normalizeChannels(channels);
72
+ this.channels = this.options.canLoadPrivateState()
73
+ ? normalizeChannels([
74
+ ...publicChannels,
75
+ ...this.channels.filter((channel) => channel.kind === "direct" || channel.kind === "group"),
76
+ ])
77
+ : publicChannels;
72
78
  })
73
79
  .catch(() => {
74
80
  // Keep the last backend-provided list if the refresh fails.
@@ -73,15 +73,15 @@ export class ChatController {
73
73
  private readonly session = createChatControllerSessionState();
74
74
  private pendingMessageSeq = 0;
75
75
  private notifyFn: (notification: AppNotificationRequest) => AppNotificationDelivery | void = () => {};
76
+ private openMessageFn: ((channelId: string, messageId: string) => void) | undefined;
76
77
  private notifiedMessageIds = new Set<string>();
77
78
 
78
79
  private readonly storage = new ChatControllerStorage({
79
80
  emit: (channelId) => this.emit(channelId),
80
- getSessionToken: () => this.session.sessionToken,
81
81
  getUser: () => this.session.user,
82
82
  });
83
83
  private readonly channelCatalog = new ChatControllerChannels({
84
- canLoadPrivateState: () => !!this.session.user?.emailVerified && !!this.session.sessionToken,
84
+ canLoadPrivateState: () => !!this.session.user?.emailVerified,
85
85
  ensureChannelState: (channelId) => this.ensureChannelState(channelId),
86
86
  getChannelStateIds: () => this.storage.channelStates.keys(),
87
87
  handleNotification: (notification, options) => this.handleChatNotification(notification, options),
@@ -94,7 +94,7 @@ export class ChatController {
94
94
  getChannelStateSnapshots: () => this.channelCatalog.getChannelStateSnapshots(),
95
95
  isChannelsLoading: () => this.channelCatalog.isLoading(),
96
96
  isSessionChecked: () => this.session.sessionChecked,
97
- hasSessionToken: () => !!this.session.sessionToken,
97
+ hasSession: () => !!this.session.sessionToken || !!this.session.user,
98
98
  getOnlineCount: () => this.channelCatalog.getOnlineCount(),
99
99
  getUser: () => this.session.user,
100
100
  getListenerSnapshot: (channelId) => this.getSnapshot(channelId),
@@ -109,7 +109,7 @@ export class ChatController {
109
109
  });
110
110
  private readonly realtime = new ChatControllerRealtime({
111
111
  getAppActive: () => this.appActive,
112
- getSessionToken: () => this.session.sessionToken,
112
+ hasSession: () => !!this.session.sessionToken || !!this.session.user,
113
113
  getUser: () => this.session.user,
114
114
  refreshSession: () => this.refreshSession(),
115
115
  handleNotification: (notification) => this.handleChatNotification(notification),
@@ -126,8 +126,12 @@ export class ChatController {
126
126
  this.hydrate();
127
127
  }
128
128
 
129
- setNotifier(notify: (notification: AppNotificationRequest) => AppNotificationDelivery | void): void {
129
+ setNotifier(
130
+ notify: (notification: AppNotificationRequest) => AppNotificationDelivery | void,
131
+ openMessage?: (channelId: string, messageId: string) => void,
132
+ ): void {
130
133
  this.notifyFn = notify;
134
+ this.openMessageFn = openMessage;
131
135
  }
132
136
 
133
137
  hydrate(): void {
@@ -375,6 +379,7 @@ export class ChatController {
375
379
  flushDraftSync: (channelId) => this.storage.flushDraftSync(channelId),
376
380
  resetNotifier: () => {
377
381
  this.notifyFn = () => {};
382
+ this.openMessageFn = undefined;
378
383
  },
379
384
  stopRealtime: () => this.realtime.stopAll(),
380
385
  });
@@ -393,7 +398,6 @@ export class ChatController {
393
398
  content,
394
399
  replyToId,
395
400
  user: this.session.user,
396
- sessionToken: this.session.sessionToken,
397
401
  ensureConnection: () => this.ensureConnection(normalizedChannelId),
398
402
  getVisibleMessages: () => this.getVisibleMessages(normalizedChannelId),
399
403
  nextPendingMessageId: () => `local:${Date.now()}:${this.pendingMessageSeq += 1}`,
@@ -409,7 +413,7 @@ export class ChatController {
409
413
  const channel = this.ensureChannelState(normalizedChannelId);
410
414
  const messageContent = content.trim();
411
415
  if (!messageContent) return false;
412
- if (!this.session.user?.emailVerified || !this.session.sessionToken) return false;
416
+ if (!this.session.user?.emailVerified) return false;
413
417
 
414
418
  const latestOwnMessage = [...getVisibleMessages(channel)]
415
419
  .reverse()
@@ -446,7 +450,7 @@ export class ChatController {
446
450
  ensureChatChannelConnection({
447
451
  channelId: normalizedChannelId,
448
452
  channel,
449
- canConnect: !!this.session.user?.emailVerified && !!this.session.sessionToken,
453
+ canConnect: !!this.session.user?.emailVerified,
450
454
  stopSafetyRefresh: () => this.realtime.stopSafetyRefresh(),
451
455
  startSafetyRefresh: () => this.realtime.startSafetyRefresh(),
452
456
  refreshMessages: () => this.refreshChannelMessages(normalizedChannelId),
@@ -529,6 +533,7 @@ export class ChatController {
529
533
  getChannel: (channelId) => this.channelCatalog.getChannels().find((channel) => channel.id === channelId),
530
534
  notifiedMessageIds: this.notifiedMessageIds,
531
535
  notify: this.notifyFn,
536
+ openMessage: this.openMessageFn,
532
537
  });
533
538
  }
534
539
 
@@ -550,7 +555,7 @@ export class ChatController {
550
555
  const channel = this.ensureChannelState(channelId);
551
556
  return markChatChannelViewedThroughLatestMessage({
552
557
  channel,
553
- canSyncReadState: !!this.session.user?.emailVerified && !!this.session.sessionToken,
558
+ canSyncReadState: !!this.session.user?.emailVerified,
554
559
  persist,
555
560
  persistChannelState: () => this.storage.persistChannelState(channelId),
556
561
  syncReadState: (messageId) => {
@@ -20,11 +20,13 @@ function notifyChatServerMessage({
20
20
  channel,
21
21
  notifiedMessageIds,
22
22
  notify,
23
+ openMessage,
23
24
  }: {
24
25
  notification: ChatNotification;
25
26
  channel: ChatChannel | undefined;
26
27
  notifiedMessageIds: Set<string>;
27
28
  notify: (notification: AppNotificationRequest) => AppNotificationDelivery | void;
29
+ openMessage?: (channelId: string, messageId: string) => void;
28
30
  }): boolean {
29
31
  if (notifiedMessageIds.has(notification.messageId)) return true;
30
32
  const channelTitle = formatChatPaneTitle(channel, notification.channelId);
@@ -32,13 +34,18 @@ function notifyChatServerMessage({
32
34
  ? formatReplyToast(notification.message)
33
35
  : notification.type === "mention"
34
36
  ? formatMentionToast(notification.message)
35
- : formatChannelToast(channelTitle, notification.message, channel?.kind);
37
+ : formatChannelToast(notification.message, channel?.kind === "direct");
36
38
  const delivered = wasNotificationDelivered(notify({
37
- title: "Gloomberb chat",
38
- subtitle: channelTitle,
39
+ title: channelTitle,
39
40
  body,
40
41
  type: "info",
41
42
  desktop: "when-inactive",
43
+ ...(openMessage ? {
44
+ action: {
45
+ label: "Open",
46
+ onClick: () => openMessage(notification.channelId, notification.messageId),
47
+ },
48
+ } : {}),
42
49
  }));
43
50
  if (delivered) {
44
51
  notifiedMessageIds.add(notification.messageId);
@@ -55,6 +62,7 @@ export function handleChatNotification({
55
62
  getChannel,
56
63
  notifiedMessageIds,
57
64
  notify,
65
+ openMessage,
58
66
  }: {
59
67
  notification: ChatNotification;
60
68
  options?: { countUnread?: boolean };
@@ -64,6 +72,7 @@ export function handleChatNotification({
64
72
  getChannel: (channelId: string) => ChatChannel | undefined;
65
73
  notifiedMessageIds: Set<string>;
66
74
  notify: (notification: AppNotificationRequest) => AppNotificationDelivery | void;
75
+ openMessage?: (channelId: string, messageId: string) => void;
67
76
  }): void {
68
77
  mergeMessages(notification.channelId, [notification.message], { countUnread: options.countUnread });
69
78
  const channel = ensureChannelState(notification.channelId);
@@ -73,6 +82,7 @@ export function handleChatNotification({
73
82
  channel: getChannel(notification.channelId),
74
83
  notifiedMessageIds,
75
84
  notify,
85
+ openMessage,
76
86
  });
77
87
  if (activelyViewed) {
78
88
  notifiedMessageIds.add(notification.messageId);
@@ -57,14 +57,12 @@ export function hydratePersistedChannelState({
57
57
  persistence,
58
58
  resume,
59
59
  userId,
60
- sessionToken,
61
60
  }: {
62
61
  channelId: string;
63
62
  channel: ChannelRuntimeState;
64
63
  persistence: PluginPersistence | null;
65
64
  resume: PluginResumeState | null;
66
65
  userId: string | null;
67
- sessionToken: string | null;
68
66
  }): void {
69
67
  if (channel.hydrated || !persistence) return;
70
68
  channel.hydrated = true;
@@ -84,7 +82,6 @@ export function hydratePersistedChannelState({
84
82
  messages: transcript?.value.messages ?? [],
85
83
  persistedChannel,
86
84
  userId,
87
- sessionToken,
88
85
  });
89
86
  }
90
87
 
@@ -6,7 +6,7 @@ import {
6
6
 
7
7
  interface ChatControllerRealtimeOptions {
8
8
  getAppActive: () => boolean;
9
- getSessionToken: () => string | null;
9
+ hasSession: () => boolean;
10
10
  getUser: () => { emailVerified?: boolean } | null;
11
11
  refreshSession: () => Promise<void>;
12
12
  handleNotification: (notification: ChatNotification) => void;
@@ -26,7 +26,7 @@ export class ChatControllerRealtime {
26
26
 
27
27
  syncVerificationPolling(): void {
28
28
  const user = this.options.getUser();
29
- if (!this.options.getAppActive() || !this.options.getSessionToken() || !user || user.emailVerified) {
29
+ if (!this.options.getAppActive() || !this.options.hasSession() || !user || user.emailVerified) {
30
30
  this.stopVerificationPolling();
31
31
  return;
32
32
  }
@@ -66,7 +66,7 @@ export class ChatControllerRealtime {
66
66
  startSafetyRefresh(): void {
67
67
  if (this.safetyRefreshTimer) return;
68
68
  this.safetyRefreshTimer = setInterval(() => {
69
- if (!this.options.getUser()?.emailVerified || !this.options.getSessionToken()) {
69
+ if (!this.options.getUser()?.emailVerified || !this.options.hasSession()) {
70
70
  this.stopSafetyRefresh();
71
71
  return;
72
72
  }
@@ -14,7 +14,6 @@ interface SendChannelMessageOptions {
14
14
  content: string;
15
15
  replyToId?: string;
16
16
  user: ChatSessionUser | null;
17
- sessionToken: string | null;
18
17
  ensureConnection: () => void;
19
18
  getVisibleMessages: () => ChatMessage[];
20
19
  nextPendingMessageId: () => string;
@@ -30,7 +29,6 @@ export function sendChatMessageToChannel({
30
29
  content,
31
30
  replyToId,
32
31
  user,
33
- sessionToken,
34
32
  ensureConnection,
35
33
  getVisibleMessages,
36
34
  nextPendingMessageId,
@@ -41,7 +39,7 @@ export function sendChatMessageToChannel({
41
39
  }: SendChannelMessageOptions): boolean {
42
40
  const messageContent = content.trim();
43
41
  if (!messageContent) return false;
44
- if (!user?.emailVerified || !sessionToken) return false;
42
+ if (!user?.emailVerified) return false;
45
43
  if (hasPendingSend(channel, messageContent, replyToId)) return true;
46
44
  if (!channel.wsConnection) {
47
45
  ensureConnection();
@@ -46,13 +46,14 @@ export function hydrateChatControllerSession({
46
46
  session.hydrated = true;
47
47
 
48
48
  const storedSession = storage.readSessionState();
49
+ const restoredUser = apiClient.getCurrentUser() ?? storedSession?.user ?? null;
49
50
  session.sessionToken = storedSession?.sessionToken ?? null;
50
51
  apiClient.setSessionToken(session.sessionToken);
51
52
  // WebSocket tokens are short-lived connection credentials. Reusing a persisted
52
53
  // one can trap reconnects on an expired token even while the session cookie is valid.
53
54
  apiClient.setWebSocketToken(null);
54
- apiClient.restoreCachedUser(storedSession?.user ?? null);
55
- session.user = normalizeSessionUser(storedSession?.user);
55
+ apiClient.restoreCachedUser(restoredUser);
56
+ session.user = normalizeSessionUser(restoredUser);
56
57
  session.sessionChecked = true;
57
58
  storage.ensureChannelState(DEFAULT_CHAT_CHANNEL_ID);
58
59
  syncVerificationPolling();
@@ -115,17 +116,23 @@ export async function refreshChatControllerSession({
115
116
  stopVerificationPolling,
116
117
  syncVerificationPolling,
117
118
  }: RefreshChatControllerSessionOptions): Promise<void> {
118
- const token = apiClient.getSessionToken();
119
- session.sessionToken = token;
120
- if (!token) {
121
- applySignedOut();
122
- return;
123
- }
124
-
119
+ session.sessionToken = apiClient.getSessionToken();
125
120
  const apiSession = await apiClient.getSession();
126
121
  if (!apiSession) {
127
- apiClient.setSessionToken(null);
128
- session.sessionToken = null;
122
+ const persistedToken = apiClient.getSessionToken() || session.sessionToken;
123
+ // A 200 with no user is what /auth/get-session returns when no cookie
124
+ // reached the server. Desktop's backend process used to treat that as a
125
+ // real sign-out and persist a wiped token, so the next launch was logged
126
+ // out. Keep a captured native session; hard account-missing responses
127
+ // already clear the token inside getSession().
128
+ if (persistedToken) {
129
+ session.sessionToken = persistedToken;
130
+ session.user = session.user ?? normalizeSessionUser(apiClient.getCurrentUser());
131
+ session.sessionChecked = true;
132
+ persistSession(session.sessionToken, session.user);
133
+ emit();
134
+ return;
135
+ }
129
136
  applySignedOut();
130
137
  return;
131
138
  }
@@ -155,13 +155,11 @@ export function hydrateChannelRuntimeState({
155
155
  messages,
156
156
  persistedChannel,
157
157
  userId,
158
- sessionToken,
159
158
  }: {
160
159
  channel: ChannelRuntimeState;
161
160
  messages: ChatMessage[];
162
161
  persistedChannel: PersistedChannelState | null;
163
162
  userId: string | null;
164
- sessionToken: string | null;
165
163
  }): void {
166
164
  channel.draft = persistedChannel?.draft ?? "";
167
165
  channel.draftClientMessageId = channel.draft.trim()
@@ -170,7 +168,7 @@ export function hydrateChannelRuntimeState({
170
168
  channel.replyToId = persistedChannel?.replyToId ?? null;
171
169
  channel.messages = messages;
172
170
  channel.lastCursor = resolveHydratedCursor(messages, persistedChannel?.lastCursor ?? null);
173
- channel.lastViewedMessageId = userId && sessionToken
171
+ channel.lastViewedMessageId = userId
174
172
  ? persistedChannel?.lastViewedMessageId ?? getLatestMessageId(messages)
175
173
  : null;
176
174
  }
@@ -22,7 +22,6 @@ import {
22
22
 
23
23
  interface ChatControllerStorageOptions {
24
24
  emit: (channelId?: string) => void;
25
- getSessionToken: () => string | null;
26
25
  getUser: () => ChatSessionUser | null;
27
26
  }
28
27
 
@@ -120,7 +119,6 @@ export class ChatControllerStorage {
120
119
  persistence: this.persistence,
121
120
  resume: this.resume,
122
121
  userId: this.options.getUser()?.id ?? null,
123
- sessionToken: this.options.getSessionToken(),
124
122
  });
125
123
  }
126
124
 
@@ -1,4 +1,4 @@
1
- import type { ChatChannel, ChatMessage } from "../../../../api-client";
1
+ import type { ChatMessage } from "../../../../api-client";
2
2
  import { t, tf } from "../../../../i18n";
3
3
 
4
4
  const ISO_TIMESTAMP_CURSOR = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
@@ -43,13 +43,11 @@ export function formatReplyToast(message: ChatMessage): string {
43
43
  return tf("@{author} replied to you: {snippet}", { author, snippet });
44
44
  }
45
45
 
46
- export function formatChannelToast(channelTitle: string, message: ChatMessage, channelKind?: ChatChannel["kind"]): string {
46
+ export function formatChannelToast(message: ChatMessage, direct = false): string {
47
47
  const author = message.user.username || t("Someone");
48
48
  const snippet = formatMessageSnippet(message.content);
49
- if (channelKind === "direct") {
50
- return snippet ? `@${author}: ${snippet}` : tf("@{author} sent a message.", { author });
51
- }
52
- return snippet ? `${channelTitle} @${author}: ${snippet}` : tf("{channel} @{author} sent a message.", { channel: channelTitle, author });
49
+ if (direct && snippet) return snippet;
50
+ return snippet ? `@${author}: ${snippet}` : tf("@{author} sent a message.", { author });
53
51
  }
54
52
 
55
53
  export function isLegacyTimestampCursor(cursor: string | null): boolean {
@@ -16,7 +16,7 @@ interface ChatControllerViewOptions {
16
16
  getChannelStateSnapshots: () => ChatChannelState[];
17
17
  isChannelsLoading: () => boolean;
18
18
  isSessionChecked: () => boolean;
19
- hasSessionToken: () => boolean;
19
+ hasSession: () => boolean;
20
20
  getOnlineCount: () => number;
21
21
  getUser: () => ChatControllerSnapshot["user"];
22
22
  getListenerSnapshot: (channelId: string) => ChatControllerSnapshot;
@@ -69,7 +69,7 @@ export class ChatControllerView {
69
69
  messagesError: channel.messagesError,
70
70
  loadingOlderMessages: channel.olderMessagesLoading,
71
71
  hasOlderMessages: channel.messages.length > 0 && !channel.reachedOldestMessage,
72
- hasSavedSession: this.options.hasSessionToken(),
72
+ hasSavedSession: this.options.hasSession(),
73
73
  onlineCount: this.options.getOnlineCount(),
74
74
  user: this.options.getUser(),
75
75
  messages: this.options.getVisibleMessages(normalizedChannelId),