gloomberb 0.9.9 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/README.md +41 -3
  2. package/README.zh-CN.md +294 -0
  3. package/package.json +10 -4
  4. package/patches/@opentui%2Fcore@0.3.2.patch +26 -69
  5. package/src/api-client/auth.ts +2 -0
  6. package/src/api-client/index.test.ts +4 -0
  7. package/src/api-client/types.ts +3 -0
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -0
  9. package/src/app/pane-template-dialog-wizard.test.tsx +36 -0
  10. package/src/app/pane-template-dialog-wizard.tsx +10 -3
  11. package/src/app/runtime/desktop-deeplink.test.ts +15 -0
  12. package/src/app/runtime/desktop-deeplink.ts +21 -6
  13. package/src/app.tsx +40 -33
  14. package/src/capabilities/service-capabilities.ts +7 -2
  15. package/src/cli/commands/automation.test.ts +202 -0
  16. package/src/cli/commands/automation.ts +153 -36
  17. package/src/cli/commands/market.ts +1 -14
  18. package/src/cli/commands/system.ts +4 -1
  19. package/src/cli/context.ts +1 -1
  20. package/src/cli/desktop-pane-shot.ts +65 -4
  21. package/src/cli/entry.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +532 -0
  23. package/src/cli/pane-functions/catalog.ts +39 -4
  24. package/src/cli/pane-functions/data.test.ts +17 -0
  25. package/src/cli/pane-functions/data.ts +55 -13
  26. package/src/cli/pane-functions/discovery.ts +36 -9
  27. package/src/cli/pane-functions/index.test.ts +104 -1
  28. package/src/cli/pane-functions/index.ts +64 -5
  29. package/src/cli/pane-functions/options.ts +21 -6
  30. package/src/cli/pane-functions/report.ts +735 -72
  31. package/src/cli/pane-functions/resolver.ts +70 -6
  32. package/src/cli/pane-functions/screenshot.test.ts +431 -0
  33. package/src/cli/pane-functions/screenshot.ts +952 -8
  34. package/src/components/chart/composite/column-layout.ts +215 -0
  35. package/src/components/chart/composite/composite-chart.test.tsx +630 -0
  36. package/src/components/chart/composite/composite-chart.tsx +1148 -0
  37. package/src/components/chart/composite/format.test.ts +137 -0
  38. package/src/components/chart/composite/format.ts +135 -0
  39. package/src/components/chart/composite/index.ts +38 -0
  40. package/src/components/chart/composite/interactions.test.ts +109 -0
  41. package/src/components/chart/composite/interactions.ts +247 -0
  42. package/src/components/chart/composite/price-series.test.ts +39 -0
  43. package/src/components/chart/composite/price-series.ts +78 -0
  44. package/src/components/chart/composite/rasterizer.ts +359 -0
  45. package/src/components/chart/composite/renderers.test.ts +610 -0
  46. package/src/components/chart/composite/scene.test.ts +464 -0
  47. package/src/components/chart/composite/scene.ts +553 -0
  48. package/src/components/chart/composite/text-renderer.ts +285 -0
  49. package/src/components/chart/composite/time-scale.test.ts +88 -0
  50. package/src/components/chart/composite/time-scale.ts +217 -0
  51. package/src/components/chart/composite/types.ts +141 -0
  52. package/src/components/chart/core/data.ts +81 -5
  53. package/src/components/chart/core/index.test.ts +128 -43
  54. package/src/components/chart/core/resolution.ts +0 -6
  55. package/src/components/chart/native/chart-rasterizer.ts +1 -3
  56. package/src/components/chart/native/kitty/index.test.ts +99 -120
  57. package/src/components/chart/native/kitty/manager.ts +3 -0
  58. package/src/components/chart/native/kitty/protocol.ts +4 -1
  59. package/src/components/chart/native/raster/crosshair.ts +79 -46
  60. package/src/components/chart/native/raster/types.ts +2 -11
  61. package/src/components/chart/native/surface/manager.ts +48 -3
  62. package/src/components/chart/price-axis-labels.tsx +18 -4
  63. package/src/components/command-bar/commands/direct/index.ts +25 -0
  64. package/src/components/command-bar/commands/registry.ts +9 -0
  65. package/src/components/command-bar/helpers.test.ts +4 -1
  66. package/src/components/command-bar/list/view.tsx +18 -6
  67. package/src/components/command-bar/multi-select-picker.tsx +6 -5
  68. package/src/components/command-bar/pane-settings.test.ts +141 -0
  69. package/src/components/command-bar/pane-settings.ts +61 -1
  70. package/src/components/command-bar/panel/body.tsx +7 -6
  71. package/src/components/command-bar/routes/root/command-items.ts +4 -4
  72. package/src/components/command-bar/routes/root/results.ts +10 -1
  73. package/src/components/command-bar/routes/root/selection.test.ts +54 -0
  74. package/src/components/command-bar/routes/root/selection.ts +13 -0
  75. package/src/components/command-bar/routing/actions.ts +18 -2
  76. package/src/components/command-bar/routing/list-state.ts +2 -1
  77. package/src/components/command-bar/surface/index.test.tsx +36 -4
  78. package/src/components/command-bar/surface/pane.test.tsx +93 -0
  79. package/src/components/command-bar/theme-picker.tsx +2 -1
  80. package/src/components/command-bar/view-model.ts +19 -20
  81. package/src/components/command-bar/workflow/body.tsx +2 -1
  82. package/src/components/command-bar/workflow/field-row.tsx +3 -2
  83. package/src/components/command-bar/workflow/fields.ts +4 -2
  84. package/src/components/command-bar/workflow/ops.test.ts +161 -0
  85. package/src/components/command-bar/workflow/ops.ts +20 -50
  86. package/src/components/command-bar/workflow/route-actions.test.ts +34 -0
  87. package/src/components/command-bar/workflow/route-actions.ts +4 -0
  88. package/src/components/command-bar/workflow/types.ts +1 -0
  89. package/src/components/feed-data-table/stack-view.tsx +6 -5
  90. package/src/components/index.ts +13 -1
  91. package/src/components/layout/detached-pane-shell.tsx +2 -1
  92. package/src/components/layout/floating-pane.tsx +3 -0
  93. package/src/components/layout/header.tsx +15 -8
  94. package/src/components/layout/pane/footer/index.test.tsx +22 -3
  95. package/src/components/layout/pane/footer/index.tsx +3 -1
  96. package/src/components/layout/pane/footer/model.ts +9 -2
  97. package/src/components/layout/pane/header.tsx +4 -6
  98. package/src/components/layout/pane/index.tsx +3 -0
  99. package/src/components/layout/pane/sidebar.test.tsx +100 -0
  100. package/src/components/layout/pane/sidebar.tsx +239 -0
  101. package/src/components/layout/pane/title.ts +4 -3
  102. package/src/components/layout/shell/action-menu-overlay.tsx +5 -3
  103. package/src/components/layout/shell/drag/runtime.ts +2 -0
  104. package/src/components/layout/shell/menu.test.ts +19 -0
  105. package/src/components/layout/shell/menu.ts +8 -5
  106. package/src/components/layout/shell/native/pointer-runtime.ts +4 -1
  107. package/src/components/layout/shell/pane/layers.tsx +2 -2
  108. package/src/components/layout/status-bar.tsx +8 -7
  109. package/src/components/layout/window-controls.tsx +4 -3
  110. package/src/components/metric-treemap/index.tsx +3 -2
  111. package/src/components/onboarding/onboarding-steps.tsx +30 -29
  112. package/src/components/onboarding/onboarding-wizard.tsx +5 -4
  113. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +15 -12
  114. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +17 -16
  115. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +6 -5
  116. package/src/components/onboarding/portfolio-step/choose-panel.tsx +5 -4
  117. package/src/components/pane-settings-dialog/desktop.tsx +25 -3
  118. package/src/components/pane-settings-dialog/field-dialogs.tsx +5 -4
  119. package/src/components/pane-settings-dialog/tui.tsx +11 -9
  120. package/src/components/pane-settings-dialog/value.ts +8 -5
  121. package/src/components/pane-settings-dialog.test.tsx +235 -0
  122. package/src/components/pane-settings-dialog.tsx +24 -0
  123. package/src/components/pane-template-wizard.tsx +6 -2
  124. package/src/components/ticker/list-table-view.tsx +3 -2
  125. package/src/components/ui/button.tsx +3 -1
  126. package/src/components/ui/choice-dialog.tsx +6 -1
  127. package/src/components/ui/confirm-dialog.tsx +3 -2
  128. package/src/components/ui/fields.tsx +10 -1
  129. package/src/components/ui/frame.tsx +4 -1
  130. package/src/components/ui/index.ts +5 -0
  131. package/src/components/ui/inline-quick-add.tsx +104 -0
  132. package/src/components/ui/list-view.tsx +5 -4
  133. package/src/components/ui/loading.tsx +2 -1
  134. package/src/components/ui/multi-select/dialog.tsx +188 -44
  135. package/src/components/ui/popover.tsx +10 -0
  136. package/src/components/ui/shortcut-hint.tsx +2 -0
  137. package/src/components/ui/status.tsx +4 -3
  138. package/src/components/ui/tabs.tsx +19 -8
  139. package/src/components/ui/toggle.tsx +44 -3
  140. package/src/components/ui/ui.test.tsx +67 -5
  141. package/src/core/app-services.ts +4 -6
  142. package/src/core/state/session-persistence.ts +3 -2
  143. package/src/data/config/chart-settings.ts +712 -0
  144. package/src/data/config/layout.ts +24 -5
  145. package/src/data/config/store/index.test.ts +377 -14
  146. package/src/data/config/store/normalize.ts +79 -65
  147. package/src/data/fred-series.ts +137 -0
  148. package/src/i18n/index.test.ts +114 -0
  149. package/src/i18n/index.ts +147 -0
  150. package/src/i18n/ja.ts +1013 -0
  151. package/src/i18n/ko.ts +1011 -0
  152. package/src/i18n/languages.test.ts +31 -0
  153. package/src/i18n/languages.ts +72 -0
  154. package/src/i18n/zh-cn.ts +1013 -0
  155. package/src/i18n/zh-tw.ts +1013 -0
  156. package/src/plugins/builtin/account-management/footer.ts +3 -2
  157. package/src/plugins/builtin/account-management/form-components.tsx +3 -2
  158. package/src/plugins/builtin/account-management/keyboard.ts +6 -0
  159. package/src/plugins/builtin/account-management/model.ts +25 -21
  160. package/src/plugins/builtin/account-management/navigation.test.ts +27 -0
  161. package/src/plugins/builtin/account-management/navigation.ts +24 -0
  162. package/src/plugins/builtin/account-management/pane.tsx +120 -85
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +10 -9
  164. package/src/plugins/builtin/ai/agent-history.ts +139 -0
  165. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +133 -51
  166. package/src/plugins/builtin/ai/ask-ai.test.tsx +83 -22
  167. package/src/plugins/builtin/ai/index.test.ts +302 -0
  168. package/src/plugins/builtin/ai/index.tsx +374 -40
  169. package/src/plugins/builtin/ai/pane-settings.test.ts +155 -0
  170. package/src/plugins/builtin/ai/pane-settings.ts +319 -0
  171. package/src/plugins/builtin/ai/pi/credential-store.test.ts +80 -0
  172. package/src/plugins/builtin/ai/pi/credential-store.ts +138 -0
  173. package/src/plugins/builtin/ai/pi/file-store.ts +138 -0
  174. package/src/plugins/builtin/ai/pi/host.test.ts +500 -0
  175. package/src/plugins/builtin/ai/pi/host.ts +564 -0
  176. package/src/plugins/builtin/ai/pi/index.ts +5 -0
  177. package/src/plugins/builtin/ai/pi/models-store.test.ts +44 -0
  178. package/src/plugins/builtin/ai/pi/models-store.ts +141 -0
  179. package/src/plugins/builtin/ai/pi/providers.ts +68 -0
  180. package/src/plugins/builtin/ai/pi/runtime.test.ts +240 -0
  181. package/src/plugins/builtin/ai/pi/runtime.ts +635 -0
  182. package/src/plugins/builtin/ai/providers.test.ts +68 -0
  183. package/src/plugins/builtin/ai/providers.ts +163 -38
  184. package/src/plugins/builtin/ai/runner-selection.test.ts +174 -0
  185. package/src/plugins/builtin/ai/runner-selection.ts +193 -0
  186. package/src/plugins/builtin/ai/runner-selector.tsx +153 -0
  187. package/src/plugins/builtin/ai/runner.test.ts +198 -0
  188. package/src/plugins/builtin/ai/runner.ts +276 -73
  189. package/src/plugins/builtin/ai/screener/contract.test.ts +26 -73
  190. package/src/plugins/builtin/ai/screener/contract.ts +14 -52
  191. package/src/plugins/builtin/ai/screener/editor-runtime.ts +55 -5
  192. package/src/plugins/builtin/ai/screener/editor.tsx +42 -36
  193. package/src/plugins/builtin/ai/screener/footer.ts +82 -67
  194. package/src/plugins/builtin/ai/screener/keyboard.ts +27 -11
  195. package/src/plugins/builtin/ai/screener/model.test.ts +24 -0
  196. package/src/plugins/builtin/ai/screener/model.ts +9 -3
  197. package/src/plugins/builtin/ai/screener/pane.test.tsx +142 -55
  198. package/src/plugins/builtin/ai/screener/pane.tsx +95 -97
  199. package/src/plugins/builtin/ai/screener/results-view.tsx +8 -41
  200. package/src/plugins/builtin/ai/screener/runner.ts +41 -25
  201. package/src/plugins/builtin/ai/screener/tabs-bar.tsx +2 -2
  202. package/src/plugins/builtin/ai/use-runtime-providers.ts +28 -0
  203. package/src/plugins/builtin/ai/workspace/model.test.ts +227 -0
  204. package/src/plugins/builtin/ai/workspace/model.ts +308 -0
  205. package/src/plugins/builtin/ai/workspace/pane.test.tsx +197 -0
  206. package/src/plugins/builtin/ai/workspace/pane.tsx +896 -0
  207. package/src/plugins/builtin/analytics/index.test.tsx +3 -3
  208. package/src/plugins/builtin/analytics/index.tsx +3 -8
  209. package/src/plugins/builtin/broker-manager/detail.tsx +25 -24
  210. package/src/plugins/builtin/broker-manager/index.test.tsx +1 -1
  211. package/src/plugins/builtin/broker-manager/index.tsx +9 -11
  212. package/src/plugins/builtin/broker-manager/model.ts +11 -10
  213. package/src/plugins/builtin/broker-manager/pane-actions.tsx +22 -21
  214. package/src/plugins/builtin/broker-manager/table.ts +9 -8
  215. package/src/plugins/builtin/changelog/index.tsx +3 -8
  216. package/src/plugins/builtin/chart-composer/chart-spec.ts +89 -0
  217. package/src/plugins/builtin/chart-composer/cli-options.ts +98 -0
  218. package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +120 -0
  219. package/src/plugins/builtin/chart-composer/editor.test.tsx +332 -0
  220. package/src/plugins/builtin/chart-composer/editor.tsx +1107 -0
  221. package/src/plugins/builtin/chart-composer/index.tsx +228 -0
  222. package/src/plugins/builtin/chart-composer/pane.tsx +645 -0
  223. package/src/plugins/builtin/chart-composer/presets.test.ts +561 -0
  224. package/src/plugins/builtin/chart-composer/presets.ts +759 -0
  225. package/src/plugins/builtin/chart-composer/quick-add.test.tsx +245 -0
  226. package/src/plugins/builtin/chart-composer/quick-add.tsx +409 -0
  227. package/src/plugins/builtin/chart-composer/semantic.ts +86 -0
  228. package/src/plugins/builtin/chart-composer/series-catalog.test.ts +65 -0
  229. package/src/plugins/builtin/chart-composer/series-catalog.ts +295 -0
  230. package/src/plugins/builtin/chart-composer/settings.test.ts +163 -0
  231. package/src/plugins/builtin/chart-composer/settings.ts +397 -0
  232. package/src/plugins/builtin/chart-composer/shortcuts.test.ts +44 -0
  233. package/src/plugins/builtin/chart-composer/shortcuts.ts +35 -0
  234. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +103 -0
  235. package/src/plugins/builtin/chat/channel-labels.ts +5 -4
  236. package/src/plugins/builtin/chat/channels.ts +12 -11
  237. package/src/plugins/builtin/chat/chat.test.tsx +32 -0
  238. package/src/plugins/builtin/chat/content/composer-runtime.ts +6 -1
  239. package/src/plugins/builtin/chat/content/composer.tsx +9 -8
  240. package/src/plugins/builtin/chat/content/index.tsx +36 -1
  241. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +8 -7
  242. package/src/plugins/builtin/chat/content/transcript.tsx +9 -3
  243. package/src/plugins/builtin/chat/controller/utils.ts +10 -9
  244. package/src/plugins/builtin/chat/message/desktop.tsx +17 -10
  245. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +122 -3
  246. package/src/plugins/builtin/chat/message/profile-popover.tsx +38 -1
  247. package/src/plugins/builtin/chat/message/terminal.tsx +19 -10
  248. package/src/plugins/builtin/chat/pane-state.test.ts +31 -0
  249. package/src/plugins/builtin/chat/pane-state.ts +30 -0
  250. package/src/plugins/builtin/chat/pane.tsx +27 -4
  251. package/src/plugins/builtin/chat/profile-popover.ts +62 -5
  252. package/src/plugins/builtin/chat/sidebar.test.tsx +4 -7
  253. package/src/plugins/builtin/chat/sidebar.tsx +107 -168
  254. package/src/plugins/builtin/chat/test-harness.tsx +6 -0
  255. package/src/plugins/builtin/cloud/auth-actions.tsx +5 -4
  256. package/src/plugins/builtin/cloud/plugin.tsx +150 -127
  257. package/src/plugins/builtin/composite-plugins.ts +71 -0
  258. package/src/plugins/builtin/correlation/index.test.tsx +4 -4
  259. package/src/plugins/builtin/correlation/index.tsx +3 -8
  260. package/src/plugins/builtin/earnings/index.tsx +37 -21
  261. package/src/plugins/builtin/earnings/model.test.ts +52 -0
  262. package/src/plugins/builtin/earnings/model.ts +32 -0
  263. package/src/plugins/builtin/econ/detail-view.tsx +17 -4
  264. package/src/plugins/builtin/econ/fred-cache.test.ts +57 -11
  265. package/src/plugins/builtin/econ/index.tsx +16 -20
  266. package/src/plugins/builtin/fear-greed/index.tsx +2 -8
  267. package/src/plugins/builtin/fx-matrix/index.tsx +3 -8
  268. package/src/plugins/builtin/help/components.tsx +5 -4
  269. package/src/plugins/builtin/help/index.tsx +14 -17
  270. package/src/plugins/builtin/holders/index.tsx +2 -8
  271. package/src/plugins/builtin/insider/index.tsx +2 -7
  272. package/src/plugins/builtin/kelly-sizer/index.test.tsx +2 -2
  273. package/src/plugins/builtin/kelly-sizer/index.tsx +3 -7
  274. package/src/plugins/builtin/kelly-sizer/test-support.tsx +3 -3
  275. package/src/plugins/builtin/layout-manager/index.tsx +3 -7
  276. package/src/plugins/builtin/market-heatmap/index.tsx +3 -8
  277. package/src/plugins/builtin/market-movers/index.tsx +3 -8
  278. package/src/plugins/builtin/news/index.tsx +12 -16
  279. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +3 -3
  280. package/src/plugins/builtin/news/wire/index.tsx +83 -73
  281. package/src/plugins/builtin/options/index.tsx +2 -8
  282. package/src/plugins/builtin/plugin-module.test.ts +89 -0
  283. package/src/plugins/builtin/plugin-module.ts +116 -0
  284. package/src/plugins/builtin/portfolio-list/header.tsx +4 -3
  285. package/src/plugins/builtin/portfolio-list/index.test.ts +17 -0
  286. package/src/plugins/builtin/portfolio-list/index.tsx +3 -5
  287. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +3 -3
  288. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +53 -53
  289. package/src/plugins/builtin/portfolio-list/settings.ts +5 -3
  290. package/src/plugins/builtin/portfolio-list/summary/index.tsx +4 -3
  291. package/src/plugins/builtin/research/index.tsx +2 -8
  292. package/src/plugins/builtin/sec/index.tsx +2 -8
  293. package/src/plugins/builtin/sectors/index.tsx +3 -8
  294. package/src/plugins/builtin/thirteenf/index.tsx +3 -8
  295. package/src/plugins/builtin/thirteenf/pane.test.tsx +1 -1
  296. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +49 -23
  297. package/src/plugins/builtin/ticker-detail/index.test.tsx +15 -136
  298. package/src/plugins/builtin/ticker-detail/index.tsx +2 -93
  299. package/src/plugins/builtin/ticker-detail/overview/components.tsx +6 -5
  300. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +41 -17
  301. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -3
  302. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +1 -1
  303. package/src/plugins/builtin/ticker-detail/settings.ts +3 -50
  304. package/src/plugins/builtin/ticker-research-backend-plugin.ts +17 -0
  305. package/src/plugins/builtin/ticker-research-plugin.ts +27 -0
  306. package/src/plugins/builtin/tv/channels.ts +27 -0
  307. package/src/plugins/builtin/tv/index.tsx +40 -0
  308. package/src/plugins/builtin/tv/pane.tsx +286 -0
  309. package/src/plugins/builtin/tv/youtube-stream.ts +100 -0
  310. package/src/plugins/builtin/world-indices/index.tsx +3 -8
  311. package/src/plugins/builtin/yield-curve/index.tsx +8 -8
  312. package/src/plugins/catalog-backend.test.ts +21 -0
  313. package/src/plugins/catalog-backend.ts +42 -0
  314. package/src/plugins/catalog-ui.ts +6 -14
  315. package/src/plugins/ibkr/instance-selection.test.ts +0 -1
  316. package/src/plugins/ownership.ts +143 -0
  317. package/src/plugins/prediction-markets/backend-plugin.ts +19 -0
  318. package/src/plugins/prediction-markets/chart.test.tsx +7 -5
  319. package/src/plugins/prediction-markets/chart.tsx +25 -7
  320. package/src/plugins/registry/index.test.ts +51 -0
  321. package/src/plugins/registry/index.ts +8 -0
  322. package/src/plugins/registry/pane-settings.ts +1 -1
  323. package/src/plugins/runtime/index.test.tsx +2 -2
  324. package/src/remote/app-host.tsx +16 -3
  325. package/src/remote/controller.test.ts +102 -1
  326. package/src/remote/controller.ts +122 -0
  327. package/src/remote/types.ts +11 -0
  328. package/src/renderers/electrobun/bun/core-capabilities.ts +123 -27
  329. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +5 -1
  330. package/src/renderers/electrobun/bun/desktop/initialization.ts +5 -1
  331. package/src/renderers/electrobun/bun/desktop/media.ts +15 -0
  332. package/src/renderers/electrobun/bun/desktop/window-style.ts +4 -1
  333. package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +32 -1
  334. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +10 -0
  335. package/src/renderers/electrobun/bun/index.ts +5 -0
  336. package/src/renderers/electrobun/bun/tui-entry.ts +1 -6
  337. package/src/renderers/electrobun/view/ai-host.ts +124 -19
  338. package/src/renderers/electrobun/view/backend-rpc.ts +4 -0
  339. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +151 -39
  340. package/src/renderers/electrobun/view/desktop/popover.tsx +166 -0
  341. package/src/renderers/electrobun/view/dialog-host.test.ts +25 -0
  342. package/src/renderers/electrobun/view/dialog-host.tsx +75 -12
  343. package/src/renderers/electrobun/view/host/box.tsx +5 -0
  344. package/src/renderers/electrobun/view/host/chart-surface.tsx +10 -9
  345. package/src/renderers/electrobun/view/host/input.tsx +9 -19
  346. package/src/renderers/electrobun/view/host/media-surface.tsx +170 -0
  347. package/src/renderers/electrobun/view/host/mouse.ts +1 -0
  348. package/src/renderers/electrobun/view/host/style.ts +1 -1
  349. package/src/renderers/electrobun/view/host/tabs.tsx +4 -3
  350. package/src/renderers/electrobun/view/main.tsx +10 -3
  351. package/src/renderers/electrobun/view/styles.css +17 -0
  352. package/src/renderers/electrobun/view/ui-host.tsx +7 -0
  353. package/src/renderers/opentui/chart-surface.test.tsx +52 -6
  354. package/src/renderers/opentui/chart-surface.tsx +95 -11
  355. package/src/renderers/opentui/dialog-host.tsx +36 -5
  356. package/src/renderers/opentui/host.tsx +39 -0
  357. package/src/renderers/opentui/start.tsx +30 -0
  358. package/src/renderers/opentui/ui-host.tsx +2 -0
  359. package/src/sources/provider-router/index.test.ts +43 -0
  360. package/src/sources/provider-router/index.ts +4 -0
  361. package/src/sources/provider-router/supplemental.ts +46 -0
  362. package/src/sources/sec-edgar.test.ts +203 -2
  363. package/src/sources/sec-edgar.ts +70 -15
  364. package/src/state/app/context/index.tsx +13 -1
  365. package/src/state/app/context/selector.test.tsx +62 -1
  366. package/src/state/session-persistence.test.ts +13 -2
  367. package/src/sync/controller.test.ts +190 -3
  368. package/src/sync/controller.ts +76 -45
  369. package/src/sync/core-contributors.test.ts +121 -0
  370. package/src/sync/core-contributors.ts +104 -27
  371. package/src/sync/react.ts +6 -8
  372. package/src/sync/types.ts +3 -0
  373. package/src/time-series/alignment.ts +158 -0
  374. package/src/time-series/economic.ts +49 -0
  375. package/src/time-series/field-catalog.ts +306 -0
  376. package/src/time-series/fundamentals.test.ts +505 -0
  377. package/src/time-series/fundamentals.ts +842 -0
  378. package/src/time-series/hooks.ts +45 -0
  379. package/src/time-series/index.ts +14 -0
  380. package/src/time-series/live-quotes.test.ts +219 -0
  381. package/src/time-series/live-quotes.ts +203 -0
  382. package/src/time-series/market-resolution.ts +53 -0
  383. package/src/time-series/market.ts +125 -0
  384. package/src/time-series/reporting.ts +98 -0
  385. package/src/time-series/resolve.test.ts +1281 -0
  386. package/src/time-series/resolve.ts +783 -0
  387. package/src/time-series/spec-studies.test.ts +400 -0
  388. package/src/time-series/spec.ts +487 -0
  389. package/src/time-series/studies.ts +563 -0
  390. package/src/time-series/transforms.test.ts +105 -0
  391. package/src/time-series/transforms.ts +152 -0
  392. package/src/time-series/types.ts +162 -0
  393. package/src/time-series/use-chart-resolution.ts +166 -0
  394. package/src/types/config.ts +9 -11
  395. package/src/types/desktop-window.ts +1 -0
  396. package/src/types/financials.ts +4 -0
  397. package/src/types/media.ts +17 -0
  398. package/src/types/plugin.ts +32 -1
  399. package/src/ui/context-menu.tsx +15 -3
  400. package/src/ui/dialog.tsx +14 -2
  401. package/src/ui/host.tsx +40 -1
  402. package/src/ui/index.tsx +9 -0
  403. package/src/utils/exchanges.test.ts +13 -1
  404. package/src/utils/exchanges.ts +10 -0
  405. package/src/utils/financial-statements.test.ts +138 -0
  406. package/src/utils/financial-statements.ts +115 -8
  407. package/src/utils/format.test.ts +11 -1
  408. package/src/utils/format.ts +7 -0
  409. package/src/version.ts +1 -1
  410. package/src/components/chart/bar-chart-renderer.test.ts +0 -233
  411. package/src/components/chart/bar-chart-renderer.ts +0 -445
  412. package/src/components/chart/comparison/data.test.ts +0 -159
  413. package/src/components/chart/comparison/data.ts +0 -302
  414. package/src/components/chart/comparison/renderer.test.ts +0 -187
  415. package/src/components/chart/comparison/renderer.ts +0 -282
  416. package/src/components/chart/comparison/scene.ts +0 -178
  417. package/src/components/chart/comparison-stock-chart/axis-width.ts +0 -99
  418. package/src/components/chart/comparison-stock-chart/canvas.ts +0 -148
  419. package/src/components/chart/comparison-stock-chart/controls.ts +0 -252
  420. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -101
  421. package/src/components/chart/comparison-stock-chart/interactions.ts +0 -208
  422. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +0 -62
  423. package/src/components/chart/comparison-stock-chart/layout.tsx +0 -155
  424. package/src/components/chart/comparison-stock-chart/legend.tsx +0 -74
  425. package/src/components/chart/comparison-stock-chart/native-surfaces.ts +0 -89
  426. package/src/components/chart/comparison-stock-chart/render-data.ts +0 -227
  427. package/src/components/chart/comparison-stock-chart/render-output.ts +0 -140
  428. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +0 -128
  429. package/src/components/chart/comparison-stock-chart/sources.ts +0 -51
  430. package/src/components/chart/comparison-stock-chart/surface-runtime.ts +0 -159
  431. package/src/components/chart/comparison-stock-chart/toolbar.tsx +0 -117
  432. package/src/components/chart/comparison-stock-chart/types.ts +0 -22
  433. package/src/components/chart/comparison-stock-chart/view.tsx +0 -572
  434. package/src/components/chart/comparison-stock-chart/viewport-runtime.ts +0 -239
  435. package/src/components/chart/comparison-stock-chart.tsx +0 -24
  436. package/src/components/chart/core/axis-measure.ts +0 -33
  437. package/src/components/chart/core/controller.test.ts +0 -157
  438. package/src/components/chart/core/controller.ts +0 -349
  439. package/src/components/chart/core/indicator-selector.tsx +0 -75
  440. package/src/components/chart/core/keyboard.ts +0 -24
  441. package/src/components/chart/core/pane-settings.test.ts +0 -52
  442. package/src/components/chart/core/pane-settings.ts +0 -141
  443. package/src/components/chart/core/scroll.test.ts +0 -64
  444. package/src/components/chart/core/scroll.ts +0 -82
  445. package/src/components/chart/core/window-viewport.test.ts +0 -69
  446. package/src/components/chart/core/window-viewport.ts +0 -162
  447. package/src/components/chart/display-cursor.ts +0 -153
  448. package/src/components/chart/indicators/bands.test.ts +0 -36
  449. package/src/components/chart/indicators/bands.ts +0 -29
  450. package/src/components/chart/indicators/moving-averages.test.ts +0 -39
  451. package/src/components/chart/indicators/moving-averages.ts +0 -42
  452. package/src/components/chart/indicators/options.test.ts +0 -32
  453. package/src/components/chart/indicators/options.ts +0 -107
  454. package/src/components/chart/indicators/oscillators.test.ts +0 -58
  455. package/src/components/chart/indicators/oscillators.ts +0 -92
  456. package/src/components/chart/native/canvas-bitmaps.ts +0 -140
  457. package/src/components/chart/native/raster/comparison-chart.ts +0 -162
  458. package/src/components/chart/native/surface/hooks.ts +0 -144
  459. package/src/components/chart/native/surface/rendering.ts +0 -211
  460. package/src/components/chart/static/bar-chart-surface.test.tsx +0 -101
  461. package/src/components/chart/static/bar-chart-surface.tsx +0 -292
  462. package/src/components/chart/stock-chart/auto.ts +0 -216
  463. package/src/components/chart/stock-chart/axis-width.ts +0 -146
  464. package/src/components/chart/stock-chart/bitmaps.ts +0 -22
  465. package/src/components/chart/stock-chart/canvas.ts +0 -202
  466. package/src/components/chart/stock-chart/controls.tsx +0 -149
  467. package/src/components/chart/stock-chart/cursor.ts +0 -60
  468. package/src/components/chart/stock-chart/data-runtime.ts +0 -128
  469. package/src/components/chart/stock-chart/footer.test.ts +0 -65
  470. package/src/components/chart/stock-chart/footer.ts +0 -303
  471. package/src/components/chart/stock-chart/geometry.ts +0 -160
  472. package/src/components/chart/stock-chart/index.test.ts +0 -532
  473. package/src/components/chart/stock-chart/index.tsx +0 -29
  474. package/src/components/chart/stock-chart/indicators.ts +0 -226
  475. package/src/components/chart/stock-chart/interaction-runtime.ts +0 -44
  476. package/src/components/chart/stock-chart/interactions.ts +0 -300
  477. package/src/components/chart/stock-chart/keyboard.ts +0 -373
  478. package/src/components/chart/stock-chart/layout-metrics.ts +0 -62
  479. package/src/components/chart/stock-chart/layout.tsx +0 -177
  480. package/src/components/chart/stock-chart/native-surfaces.ts +0 -121
  481. package/src/components/chart/stock-chart/presentation.ts +0 -67
  482. package/src/components/chart/stock-chart/renderer-switch.test.tsx +0 -263
  483. package/src/components/chart/stock-chart/rendering/bounds.ts +0 -116
  484. package/src/components/chart/stock-chart/rendering/candidates.ts +0 -145
  485. package/src/components/chart/stock-chart/rendering/data.ts +0 -370
  486. package/src/components/chart/stock-chart/rendering/invalidation.ts +0 -68
  487. package/src/components/chart/stock-chart/rendering/output.ts +0 -304
  488. package/src/components/chart/stock-chart/rendering/planning.ts +0 -122
  489. package/src/components/chart/stock-chart/rendering/projection.test.tsx +0 -98
  490. package/src/components/chart/stock-chart/rendering/projection.ts +0 -112
  491. package/src/components/chart/stock-chart/rendering/state.test.ts +0 -70
  492. package/src/components/chart/stock-chart/rendering/state.ts +0 -283
  493. package/src/components/chart/stock-chart/requests.ts +0 -337
  494. package/src/components/chart/stock-chart/resolution-support.ts +0 -96
  495. package/src/components/chart/stock-chart/resolved/runtime.ts +0 -289
  496. package/src/components/chart/stock-chart/resolved.tsx +0 -359
  497. package/src/components/chart/stock-chart/settings.ts +0 -81
  498. package/src/components/chart/stock-chart/surface-runtime.ts +0 -62
  499. package/src/components/chart/stock-chart/types.ts +0 -25
  500. package/src/components/chart/stock-chart/view.tsx +0 -155
  501. package/src/components/chart/stock-chart/viewport/effects.ts +0 -293
  502. package/src/components/chart/stock-chart/viewport/index.ts +0 -144
  503. package/src/components/chart/stock-chart/viewport/runtime.ts +0 -375
  504. package/src/components/chart/time-axis-label.tsx +0 -175
  505. package/src/plugins/builtin/ai/gloomberb-cli.ts +0 -71
  506. package/src/plugins/builtin/ai/screener/action-bar.tsx +0 -58
  507. package/src/plugins/builtin/comparison-chart/index.test.tsx +0 -848
  508. package/src/plugins/builtin/comparison-chart/index.tsx +0 -194
  509. package/src/plugins/builtin/econ/fred-cache.ts +0 -106
  510. package/src/plugins/builtin/plugin-groups.ts +0 -130
  511. package/src/plugins/builtin/ticker-detail/chart-pane.tsx +0 -91
  512. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -100
  513. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +0 -296
  514. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/data.ts +0 -62
  515. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +0 -608
  516. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +0 -65
  517. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +0 -58
  518. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +0 -138
  519. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +0 -220
@@ -647,6 +647,38 @@ describe("ChatContent", () => {
647
647
  expect(bodySpan!.fg.toInts().join(",")).toBe(expectedSelectedFg);
648
648
  });
649
649
 
650
+ test("selects the exact message requested by a chat deep link", async () => {
651
+ const controller = createController({
652
+ messages: [makeMessage(1), makeMessage(2)],
653
+ });
654
+ let handled = 0;
655
+
656
+ await act(async () => {
657
+ testSetup = await testRender(createHarness(controller, {
658
+ width: 72,
659
+ height: 12,
660
+ targetMessageId: "m1",
661
+ onTargetMessageHandled: () => {
662
+ handled += 1;
663
+ },
664
+ }), {
665
+ width: 72,
666
+ height: 12,
667
+ });
668
+ });
669
+
670
+ await flushFrame();
671
+
672
+ const expectedSelectedFg = hexToRgbaInts(colors.selectedText);
673
+ const frame = setup().captureSpans();
674
+ const bodyLine = frame.lines.find((line) => lineText(line).includes("message 1"));
675
+ const bodySpan = bodyLine?.spans.find((span) => span.text.includes("message 1"));
676
+
677
+ expect(handled).toBe(1);
678
+ expect(bodySpan).toBeDefined();
679
+ expect(bodySpan!.fg.toInts().join(",")).toBe(expectedSelectedFg);
680
+ });
681
+
650
682
  test("grows the composer for multi-line drafts", async () => {
651
683
  const controller = createController();
652
684
 
@@ -6,6 +6,7 @@ import {
6
6
  formatInlinePreview,
7
7
  } from "../layout";
8
8
  import { parseChatComposerCommand } from "../composer-commands";
9
+ import { t, tf } from "../../../../i18n";
9
10
  import type { ChatContentController } from "./types";
10
11
  import { getComposerCursorOffset, moveComposerCursorToOffset } from "./composer-cursor";
11
12
 
@@ -360,7 +361,11 @@ export function useChatComposerRuntime({
360
361
  Math.max(contentWidth - " editing: ".length - COMPOSER_ACTION_WIDTH - 1, 0),
361
362
  )
362
363
  : "";
363
- const inputPlaceholder = editingMessage ? "Edit message..." : replyTo ? `Reply to @${replyTo.user.username}...` : "Type a message...";
364
+ const inputPlaceholder = editingMessage
365
+ ? t("Edit message...")
366
+ : replyTo
367
+ ? tf("Reply to @{username}...", { username: replyTo.user.username })
368
+ : t("Type a message...");
364
369
 
365
370
  return {
366
371
  beginEditLatestMessage,
@@ -1,6 +1,7 @@
1
1
  import { Box, Text, TextAttributes, type TextareaRenderable } from "../../../../ui";
2
2
  import { getMessageComposerBlockHeight, MessageComposer } from "../../../../components/ui";
3
3
  import { colors } from "../../../../theme/colors";
4
+ import { t } from "../../../../i18n";
4
5
  import type { ChatMessage } from "../../../../api-client";
5
6
  import { InlineAuthActions } from "../../cloud/auth-actions";
6
7
  import { ChatActionChip } from "../message/action-chip";
@@ -133,18 +134,18 @@ export function ChatComposerArea({
133
134
  <Box width={contentWidth} height={2} flexDirection="column">
134
135
  {!user && !hasSavedSession ? (
135
136
  <>
136
- <Text fg={colors.textDim}>Read-only chat. Log in or sign up to send.</Text>
137
+ <Text fg={colors.textDim}>{t("Read-only chat. Log in or sign up to send.")}</Text>
137
138
  <InlineAuthActions />
138
139
  </>
139
140
  ) : !user ? (
140
141
  <>
141
- <Text fg={colors.positive}>Saved login found. Log in again to send.</Text>
142
+ <Text fg={colors.positive}>{t("Saved login found. Log in again to send.")}</Text>
142
143
  <InlineAuthActions showSignup={false} />
143
144
  </>
144
145
  ) : (
145
146
  <>
146
- <Text fg={colors.positive}>Verify your email to send messages.</Text>
147
- <Text fg={colors.textDim}>Ctrl+P, then Resend Verification Email</Text>
147
+ <Text fg={colors.positive}>{t("Verify your email to send messages.")}</Text>
148
+ <Text fg={colors.textDim}>{t("Ctrl+P, then Resend Verification Email")}</Text>
148
149
  </>
149
150
  )}
150
151
  </Box>
@@ -166,11 +167,11 @@ export function ChatComposerArea({
166
167
 
167
168
  {editingMessage && (
168
169
  <Box height={1} width={contentWidth} flexDirection="row">
169
- <Text fg={colors.textMuted}> editing </Text>
170
+ <Text fg={colors.textMuted}>{` ${t("editing")} `}</Text>
170
171
  <Text fg={colors.textDim}>{editingPreview ? `: ${editingPreview}` : ""}</Text>
171
172
  <Box flexGrow={1} />
172
173
  <ChatActionChip
173
- label="Cancel"
174
+ label={t("Cancel")}
174
175
  width={COMPOSER_ACTION_WIDTH}
175
176
  onPress={cancelEditMessage}
176
177
  />
@@ -179,12 +180,12 @@ export function ChatComposerArea({
179
180
 
180
181
  {!editingMessage && replyTo && (
181
182
  <Box height={1} width={contentWidth} flexDirection="row">
182
- <Text fg={colors.textMuted}> replying to </Text>
183
+ <Text fg={colors.textMuted}>{` ${t("replying to")} `}</Text>
183
184
  <Text fg={colors.positive} attributes={TextAttributes.BOLD}>{`@${replyTo.user.username}`}</Text>
184
185
  <Text fg={colors.textDim}>{replyPreview ? `: ${replyPreview}` : ""}</Text>
185
186
  <Box flexGrow={1} />
186
187
  <ChatActionChip
187
- label="Cancel"
188
+ label={t("Cancel")}
188
189
  width={COMPOSER_ACTION_WIDTH}
189
190
  onPress={clearReplyTarget}
190
191
  />
@@ -33,6 +33,8 @@ import { useChatComposerRuntime } from "./composer-runtime";
33
33
  import { useChatMessageSelection } from "./selection-runtime";
34
34
  import type { ChatMessage } from "../../../../api-client";
35
35
  import { NewDmDialog } from "./new-dm-dialog";
36
+ import { usePluginAppActions } from "../../../runtime";
37
+ import { requestAccountManagementTab } from "../../account-management/navigation";
36
38
  import {
37
39
  CHAT_MESSAGE_EDIT_WINDOW_MS,
38
40
  findLatestEditableChatMessage,
@@ -53,6 +55,8 @@ interface ChatContentProps {
53
55
  onChannelChange?: (channelId: string) => void;
54
56
  onChannelTitleChange?: (title: string) => void;
55
57
  controller?: ChatContentController;
58
+ targetMessageId?: string;
59
+ onTargetMessageHandled?: () => void;
56
60
  }
57
61
 
58
62
  export function ChatContent({
@@ -63,8 +67,11 @@ export function ChatContent({
63
67
  onChannelChange,
64
68
  onChannelTitleChange,
65
69
  controller = chatController,
70
+ targetMessageId,
71
+ onTargetMessageHandled,
66
72
  }: ChatContentProps) {
67
73
  const dispatch = useAppDispatch();
74
+ const { showPane } = usePluginAppActions();
68
75
  const commandBarOpen = useAppSelector((state) => state.commandBarOpen);
69
76
  const channelId = normalizeChannelId(rawChannelId);
70
77
  const channelIdRef = useRef(channelId);
@@ -233,6 +240,16 @@ export function ChatContent({
233
240
  showProfilePopover,
234
241
  } = useChatProfilePopover();
235
242
 
243
+ const showUserProfilePopover = useCallback((targetUser: Parameters<typeof showProfilePopover>[0]) => {
244
+ showProfilePopover(targetUser, { ownProfile: targetUser.id === user?.id });
245
+ }, [showProfilePopover, user?.id]);
246
+
247
+ const openProfileSetup = useCallback(() => {
248
+ closeProfilePopover();
249
+ requestAccountManagementTab("profile");
250
+ showPane("account-management");
251
+ }, [closeProfilePopover, showPane]);
252
+
236
253
  const blurInput = useCallback(() => {
237
254
  setInputFocused(false);
238
255
  dispatch({ type: "SET_INPUT_CAPTURED", captured: false });
@@ -432,6 +449,23 @@ export function ChatContent({
432
449
  useDefaultControllerChannel,
433
450
  });
434
451
 
452
+ const handledTargetMessageRef = useRef<string | null>(null);
453
+ useEffect(() => {
454
+ if (!targetMessageId || loading || messages.length === 0) return;
455
+ const targetKey = `${channelId}:${targetMessageId}`;
456
+ if (handledTargetMessageRef.current === targetKey) return;
457
+ handledTargetMessageRef.current = targetKey;
458
+ jumpToMessage(targetMessageId);
459
+ onTargetMessageHandled?.();
460
+ }, [
461
+ channelId,
462
+ jumpToMessage,
463
+ loading,
464
+ messages.length,
465
+ onTargetMessageHandled,
466
+ targetMessageId,
467
+ ]);
468
+
435
469
  useChatContentShortcuts({
436
470
  beginEditLatestMessage,
437
471
  beginReplyTo,
@@ -550,10 +584,11 @@ export function ChatContent({
550
584
  scrollRef={scrollRef}
551
585
  selectedIdx={selectedIdx}
552
586
  setHoveredIdx={setHoveredIdx}
553
- showProfilePopover={showProfilePopover}
587
+ showProfilePopover={showUserProfilePopover}
554
588
  stickyTranscript={stickyTranscript}
555
589
  user={user}
556
590
  userByUsername={userByUsername}
591
+ onSetUpProfile={openProfileSetup}
557
592
  />
558
593
 
559
594
  {newDmOpen ? (
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { ListView, TextField, type ListViewItem } from "../../../../components/ui";
3
3
  import { useShortcut } from "../../../../react/input";
4
4
  import { colors, hoverBg } from "../../../../theme/colors";
5
+ import { t } from "../../../../i18n";
5
6
  import { Box, Text, TextAttributes, type InputRenderable } from "../../../../ui";
6
7
  import type { ChatUserSummary } from "../../../../api-client";
7
8
  import { isPlainKey } from "../../../../utils/keyboard";
@@ -124,7 +125,7 @@ export function NewDmDialog({
124
125
  const submittedValue = valueRef.current;
125
126
  const submittedUsernames = parseDmUsernames(submittedValue);
126
127
  if (!hasOnlyDmUsernameArgs(submittedValue) || submittedUsernames.length === 0 || submitting) {
127
- setError("Enter at least one @username.");
128
+ setError(t("Enter at least one @username."));
128
129
  return;
129
130
  }
130
131
  setSubmitting(true);
@@ -132,7 +133,7 @@ export function NewDmDialog({
132
133
  try {
133
134
  await onSubmit(submittedUsernames);
134
135
  } catch {
135
- setError("Could not start conversation.");
136
+ setError(t("Could not start conversation."));
136
137
  setSubmitting(false);
137
138
  }
138
139
  };
@@ -187,7 +188,7 @@ export function NewDmDialog({
187
188
  style={{ zIndex: 8 }}
188
189
  >
189
190
  <Box height={1} flexDirection="row">
190
- <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>New DM</Text>
191
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{t("New DM")}</Text>
191
192
  <Box flexGrow={1} />
192
193
  <Box width={3} height={1} backgroundColor={colors.panel} onMouseDown={onCancel} style={{ cursor: "pointer" }}>
193
194
  <Text fg={colors.text}> x </Text>
@@ -208,7 +209,7 @@ export function NewDmDialog({
208
209
  />
209
210
  <Box height={1}>
210
211
  <Text fg={selectedUsernames.length > 0 ? colors.textMuted : colors.textDim}>
211
- {selectedUsernames.length > 0 ? usernamesLabel(selectedUsernames, contentWidth) : "Recent users"}
212
+ {selectedUsernames.length > 0 ? usernamesLabel(selectedUsernames, contentWidth) : t("Recent users")}
212
213
  </Text>
213
214
  </Box>
214
215
  <ListView
@@ -218,7 +219,7 @@ export function NewDmDialog({
218
219
  bgColor={colors.bg}
219
220
  selectedBgColor={colors.selected}
220
221
  hoverBgColor={hoverBg()}
221
- emptyMessage="No recent users"
222
+ emptyMessage={t("No recent users")}
222
223
  selectOnHover
223
224
  onSelect={setSelectedIndex}
224
225
  onActivate={(item) => toggleCandidate(item.id)}
@@ -244,7 +245,7 @@ export function NewDmDialog({
244
245
  {error ? (
245
246
  <Text fg={colors.negative}>{truncateChannelLabel(error, contentWidth)}</Text>
246
247
  ) : (
247
- <Text fg={colors.textDim}>{selectedUsernames.length > 1 ? "Group chat" : "Direct message"}</Text>
248
+ <Text fg={colors.textDim}>{selectedUsernames.length > 1 ? t("Group chat") : t("Direct message")}</Text>
248
249
  )}
249
250
  <Box flexGrow={1} />
250
251
  <Box
@@ -255,7 +256,7 @@ export function NewDmDialog({
255
256
  style={{ cursor: canSubmit ? "pointer" : "default" }}
256
257
  >
257
258
  <Text fg={canSubmit ? colors.selectedText : colors.textDim}>
258
- {submitting ? " Starting " : " Start "}
259
+ {submitting ? ` ${t("Starting")} ` : ` ${t("Start")} `}
259
260
  </Text>
260
261
  </Box>
261
262
  </Box>
@@ -2,6 +2,7 @@ import { Box, ScrollBox, Text, type ScrollBoxRenderable } from "../../../../ui";
2
2
  import type { Dispatch, SetStateAction } from "react";
3
3
  import type { InlineTickerCatalogEntry } from "../../../../state/hooks/inline-tickers";
4
4
  import { colors } from "../../../../theme/colors";
5
+ import { t } from "../../../../i18n";
5
6
  import type { ChatMessage, ChatUserSummary } from "../../../../api-client";
6
7
  import { DesktopChatMessage } from "../message/desktop";
7
8
  import { UserProfilePopover } from "../message/profile-popover";
@@ -37,6 +38,7 @@ interface ChatTranscriptProps {
37
38
  selectedIdx: number;
38
39
  setHoveredIdx: Dispatch<SetStateAction<number | null>>;
39
40
  showProfilePopover: (user: ChatUserSummary) => void;
41
+ onSetUpProfile: () => void;
40
42
  stickyTranscript: boolean;
41
43
  user: { id: string; username: string; emailVerified: boolean } | null;
42
44
  userByUsername: Map<string, ChatUserSummary>;
@@ -69,7 +71,9 @@ export function ChatTranscript({
69
71
  setHoveredIdx,
70
72
  showProfilePopover,
71
73
  stickyTranscript,
74
+ user,
72
75
  userByUsername,
76
+ onSetUpProfile,
73
77
  }: ChatTranscriptProps) {
74
78
  return (
75
79
  <>
@@ -86,16 +90,16 @@ export function ChatTranscript({
86
90
  >
87
91
  {loadingOlderMessages && (
88
92
  <Box alignItems="center" justifyContent="center" height={1} width={contentWidth}>
89
- <Text fg={colors.textDim}>Loading earlier messages...</Text>
93
+ <Text fg={colors.textDim}>{t("Loading earlier messages...")}</Text>
90
94
  </Box>
91
95
  )}
92
96
  {loading && messages.length === 0 ? (
93
97
  <Box alignItems="center" justifyContent="center" flexGrow={1}>
94
- <Text fg={colors.textDim}>Loading...</Text>
98
+ <Text fg={colors.textDim}>{t("Loading...")}</Text>
95
99
  </Box>
96
100
  ) : messages.length === 0 && (
97
101
  <Box alignItems="center" justifyContent="center" flexGrow={1}>
98
- <Text fg={colors.textDim}>No messages yet. Be the first to say something!</Text>
102
+ <Text fg={colors.textDim}>{t("No messages yet. Be the first to say something!")}</Text>
99
103
  </Box>
100
104
  )}
101
105
  {messages.map((msg, index) => (
@@ -151,6 +155,8 @@ export function ChatTranscript({
151
155
  width={chatWidth}
152
156
  onClose={scheduleProfilePopoverClose}
153
157
  onKeepOpen={cancelProfilePopoverClose}
158
+ isOwnProfile={profilePopoverUser.id === user?.id}
159
+ onSetUpProfile={onSetUpProfile}
154
160
  />
155
161
  )}
156
162
  </>
@@ -1,4 +1,5 @@
1
1
  import type { ChatChannel, ChatMessage } from "../../../../api-client";
2
+ import { t, tf } from "../../../../i18n";
2
3
 
3
4
  const ISO_TIMESTAMP_CURSOR = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
4
5
  const USERNAME_MENTION = /(^|[^A-Za-z0-9_])@([A-Za-z][A-Za-z0-9_]{2,29})(?![A-Za-z0-9_])/g;
@@ -25,30 +26,30 @@ function formatMessageSnippet(content: string): string {
25
26
  }
26
27
 
27
28
  export function formatMentionToast(message: ChatMessage): string {
28
- const author = message.user.username || "Someone";
29
+ const author = message.user.username || t("Someone");
29
30
  const snippet = formatMessageSnippet(message.content);
30
31
  if (!snippet) {
31
- return `@${author} mentioned you in chat.`;
32
+ return tf("@{author} mentioned you in chat.", { author });
32
33
  }
33
- return `@${author} mentioned you: ${snippet}`;
34
+ return tf("@{author} mentioned you: {snippet}", { author, snippet });
34
35
  }
35
36
 
36
37
  export function formatReplyToast(message: ChatMessage): string {
37
- const author = message.user.username || "Someone";
38
+ const author = message.user.username || t("Someone");
38
39
  const snippet = formatMessageSnippet(message.content);
39
40
  if (!snippet) {
40
- return `@${author} replied to you.`;
41
+ return tf("@{author} replied to you.", { author });
41
42
  }
42
- return `@${author} replied to you: ${snippet}`;
43
+ return tf("@{author} replied to you: {snippet}", { author, snippet });
43
44
  }
44
45
 
45
46
  export function formatChannelToast(channelTitle: string, message: ChatMessage, channelKind?: ChatChannel["kind"]): string {
46
- const author = message.user.username || "Someone";
47
+ const author = message.user.username || t("Someone");
47
48
  const snippet = formatMessageSnippet(message.content);
48
49
  if (channelKind === "direct") {
49
- return snippet ? `@${author}: ${snippet}` : `@${author} sent a message.`;
50
+ return snippet ? `@${author}: ${snippet}` : tf("@{author} sent a message.", { author });
50
51
  }
51
- return snippet ? `${channelTitle} @${author}: ${snippet}` : `${channelTitle} @${author} sent a message.`;
52
+ return snippet ? `${channelTitle} @${author}: ${snippet}` : tf("{channel} @{author} sent a message.", { channel: channelTitle, author });
52
53
  }
53
54
 
54
55
  export function isLegacyTimestampCursor(cursor: string | null): boolean {
@@ -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 { t } from "../../../../i18n";
4
5
  import { MESSAGE_ACTION_WIDTH, normalizeInlinePreview } from "../layout";
5
6
  import { ChatActionChip } from "./action-chip";
6
7
  import { ResponsiveTickerBadgeText } from "./inline-tokens";
@@ -35,6 +36,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
35
36
  const showInlineEditAction = !state.grouped && canSend && canEditMessage;
36
37
  const showGroupedReplyAction = state.grouped && canSend;
37
38
  const showGroupedEditAction = state.grouped && canSend && canEditMessage;
39
+ const authorLabel = msg.user.username ?? "anon";
38
40
  const groupedActionWidth = MESSAGE_ACTION_WIDTH * Number(showGroupedReplyAction) + MESSAGE_ACTION_WIDTH * Number(showGroupedEditAction);
39
41
  const rowProps = {
40
42
  width: "100%",
@@ -94,15 +96,20 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
94
96
  height={1}
95
97
  paddingLeft={1}
96
98
  >
97
- <Text
98
- fg={state.authorColor}
99
- attributes={state.authorAttributes}
99
+ <Box
100
+ height={1}
100
101
  onMouseOver={() => onUserHover(msg.user)}
102
+ onMouseMove={() => onUserHover(msg.user)}
101
103
  onMouseOut={onUserHoverEnd}
102
- style={{ cursor: "default" }}
104
+ style={{ cursor: "pointer" }}
103
105
  >
104
- {msg.user.username ?? "anon"}
105
- </Text>
106
+ <Text
107
+ fg={state.authorColor}
108
+ attributes={state.authorAttributes}
109
+ >
110
+ {authorLabel}
111
+ </Text>
112
+ </Box>
106
113
  <Text fg={state.headerStatusColor}> {state.headerStatus}</Text>
107
114
  {(showInlineReplyAction || showInlineEditAction) && (
108
115
  <>
@@ -114,7 +121,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
114
121
  data-gloom-role="chat-message-reply-action"
115
122
  >
116
123
  <ChatActionChip
117
- label="Reply"
124
+ label={t("Reply")}
118
125
  width={MESSAGE_ACTION_WIDTH}
119
126
  emphasized={state.isSelected}
120
127
  onPress={() => beginReplyTo(index)}
@@ -128,7 +135,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
128
135
  data-gloom-role="chat-message-reply-action"
129
136
  >
130
137
  <ChatActionChip
131
- label="Edit"
138
+ label={t("Edit")}
132
139
  width={MESSAGE_ACTION_WIDTH}
133
140
  emphasized={state.isSelected}
134
141
  onPress={() => beginEditMessage(index)}
@@ -169,7 +176,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
169
176
  >
170
177
  {showGroupedReplyAction && (
171
178
  <ChatActionChip
172
- label="Reply"
179
+ label={t("Reply")}
173
180
  width={MESSAGE_ACTION_WIDTH}
174
181
  emphasized={state.isSelected}
175
182
  onPress={() => beginReplyTo(index)}
@@ -177,7 +184,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
177
184
  )}
178
185
  {showGroupedEditAction && (
179
186
  <ChatActionChip
180
- label="Edit"
187
+ label={t("Edit")}
181
188
  width={MESSAGE_ACTION_WIDTH}
182
189
  emphasized={state.isSelected}
183
190
  onPress={() => beginEditMessage(index)}
@@ -1,6 +1,30 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { ChatUserSummary } from "../../../../api-client";
3
- import { hasPublicChatProfileInfo } from "./profile-popover";
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useEffect } from "react";
3
+ import {
4
+ apiClient,
5
+ type AccountProfile,
6
+ type ChatUserSummary,
7
+ } from "../../../../api-client";
8
+ import { testRender } from "../../../../renderers/opentui/test-utils";
9
+ import { Box, Text } from "../../../../ui";
10
+ import { useChatProfilePopover } from "../profile-popover";
11
+ import {
12
+ hasPublicChatProfileInfo,
13
+ shouldOfferChatProfileSetup,
14
+ } from "./profile-popover";
15
+
16
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
17
+ const originalGetAccountProfile = apiClient.getAccountProfile.bind(apiClient);
18
+
19
+ afterEach(async () => {
20
+ apiClient.getAccountProfile = originalGetAccountProfile;
21
+ apiClient.setSessionToken(null);
22
+ if (!testSetup) return;
23
+ await act(async () => {
24
+ testSetup?.renderer.destroy();
25
+ });
26
+ testSetup = undefined;
27
+ });
4
28
 
5
29
  function makeUser(overrides: Partial<ChatUserSummary>): ChatUserSummary {
6
30
  return {
@@ -12,6 +36,55 @@ function makeUser(overrides: Partial<ChatUserSummary>): ChatUserSummary {
12
36
  };
13
37
  }
14
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
+ function OwnProfileHarness({ user }: { user: ChatUserSummary }) {
68
+ const {
69
+ profilePopoverUser,
70
+ showProfilePopover,
71
+ } = useChatProfilePopover();
72
+
73
+ useEffect(() => {
74
+ showProfilePopover(user, { ownProfile: true });
75
+ }, [showProfilePopover, user]);
76
+
77
+ return (
78
+ <Box width={50} height={1}>
79
+ <Text>
80
+ {profilePopoverUser?.portfolioAnalytics
81
+ ? JSON.stringify(profilePopoverUser.portfolioAnalytics)
82
+ : "loading"}
83
+ </Text>
84
+ </Box>
85
+ );
86
+ }
87
+
15
88
  describe("profile popover", () => {
16
89
  test("treats public portfolio analytics as hover profile information", () => {
17
90
  expect(hasPublicChatProfileInfo(makeUser({
@@ -33,4 +106,50 @@ describe("profile popover", () => {
33
106
  },
34
107
  }))).toBe(false);
35
108
  });
109
+
110
+ test("refreshes your cached chat identity from the current account profile", async () => {
111
+ apiClient.setSessionToken("token-123");
112
+ apiClient.getAccountProfile = async () => makeAccountProfile({
113
+ portfolioAnalytics: {
114
+ oneYearReturn: 0.14,
115
+ spyBeta: 1.05,
116
+ },
117
+ });
118
+
119
+ await act(async () => {
120
+ testSetup = await testRender(
121
+ <OwnProfileHarness user={makeUser({
122
+ company: "Gloom",
123
+ bio: "Made Gloomberb",
124
+ portfolioAnalytics: null,
125
+ })} />,
126
+ { width: 50, height: 8 },
127
+ );
128
+ });
129
+ const setup = testSetup;
130
+ expect(setup).toBeDefined();
131
+ if (!setup) return;
132
+ await act(async () => {
133
+ await setup.renderOnce();
134
+ await Promise.resolve();
135
+ await setup.renderOnce();
136
+ await setup.renderOnce();
137
+ });
138
+
139
+ const frame = setup.captureCharFrame();
140
+ expect(frame).toContain('"oneYearReturn":0.14');
141
+ expect(frame).toContain('"spyBeta":1.05');
142
+ });
143
+
144
+ test("offers a quiet setup action on your own empty profile", () => {
145
+ const emptyProfile = makeUser({
146
+ bio: null,
147
+ company: null,
148
+ title: null,
149
+ profilePublic: false,
150
+ });
151
+ expect(shouldOfferChatProfileSetup(emptyProfile, true)).toBe(true);
152
+ expect(shouldOfferChatProfileSetup(emptyProfile, false)).toBe(false);
153
+ expect(shouldOfferChatProfileSetup(makeUser({ bio: "Already set up" }), true)).toBe(false);
154
+ });
36
155
  });
@@ -1,6 +1,7 @@
1
+ import { useState } from "react";
1
2
  import { Box, Text } from "../../../../ui";
2
3
  import { TextAttributes } from "../../../../ui";
3
- import { colors } from "../../../../theme/colors";
4
+ import { colors, hoverBg } from "../../../../theme/colors";
4
5
  import type { ChatUserSummary, PublicPortfolioAnalytics } from "../../../../api-client";
5
6
  import { formatNumber } from "../../../../utils/format";
6
7
  import { truncateChannelLabel } from "../channels";
@@ -22,6 +23,14 @@ export function hasPublicChatProfileInfo(user: ChatUserSummary): boolean {
22
23
  return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim() || hasPortfolioAnalytics(user.portfolioAnalytics));
23
24
  }
24
25
 
26
+ export function hasChatProfileDetails(user: ChatUserSummary): boolean {
27
+ return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim());
28
+ }
29
+
30
+ export function shouldOfferChatProfileSetup(user: ChatUserSummary, isOwnProfile: boolean): boolean {
31
+ return isOwnProfile && !hasChatProfileDetails(user);
32
+ }
33
+
25
34
  function formatSignedPercent(value: number): string {
26
35
  const percent = value * 100;
27
36
  return `${percent >= 0 ? "+" : ""}${formatNumber(percent, 2)}%`;
@@ -118,12 +127,17 @@ export function UserProfilePopover({
118
127
  width,
119
128
  onClose,
120
129
  onKeepOpen,
130
+ isOwnProfile = false,
131
+ onSetUpProfile,
121
132
  }: {
122
133
  user: ChatUserSummary;
123
134
  width: number;
124
135
  onClose: () => void;
125
136
  onKeepOpen: () => void;
137
+ isOwnProfile?: boolean;
138
+ onSetUpProfile?: () => void;
126
139
  }) {
140
+ const [setupHovered, setSetupHovered] = useState(false);
127
141
  const popoverWidth = Math.max(24, Math.min(38, width - 4));
128
142
  const meta = [user.title, user.company].filter(Boolean).join(" · ");
129
143
  const bio = user.bio?.trim();
@@ -135,6 +149,7 @@ export function UserProfilePopover({
135
149
  ? Math.min(headerMetricsNaturalWidth(metrics), maxStatsWidth)
136
150
  : 0;
137
151
  const usernameWidth = Math.max(1, headerWidth - (statsWidth > 0 ? statsWidth + 1 : 0));
152
+ const showSetupAction = shouldOfferChatProfileSetup(user, isOwnProfile) && !!onSetUpProfile;
138
153
 
139
154
  return (
140
155
  <Box
@@ -171,6 +186,28 @@ export function UserProfilePopover({
171
186
  {bio}
172
187
  </Text>
173
188
  ) : null}
189
+ {showSetupAction ? (
190
+ <Box
191
+ height={1}
192
+ width={headerWidth}
193
+ backgroundColor={setupHovered ? hoverBg() : undefined}
194
+ onMouseOver={() => setSetupHovered(true)}
195
+ onMouseOut={() => setSetupHovered(false)}
196
+ onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
197
+ event?.preventDefault?.();
198
+ event?.stopPropagation?.();
199
+ onSetUpProfile?.();
200
+ }}
201
+ style={{ cursor: "pointer" }}
202
+ >
203
+ <Text
204
+ fg={setupHovered ? colors.text : colors.textMuted}
205
+ attributes={setupHovered ? TextAttributes.BOLD : 0}
206
+ >
207
+ Set up profile
208
+ </Text>
209
+ </Box>
210
+ ) : null}
174
211
  </Box>
175
212
  );
176
213
  }