gloomberb 0.16.3 → 0.16.4

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 (451) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -57,6 +57,8 @@ import {
57
57
  } from "./notifications";
58
58
  import {
59
59
  CHAT_MESSAGE_EDIT_WINDOW_LABEL,
60
+ DISCORD_MESSAGE_EDIT_NOTICE,
61
+ isEditableOnGloom,
60
62
  isWithinChatMessageEditWindow,
61
63
  } from "../edit-window";
62
64
  import { ChatControllerRealtime } from "./realtime";
@@ -65,6 +67,7 @@ import { ChatControllerView } from "./view";
65
67
  import { ChatControllerMessageLoading } from "./message-loading";
66
68
  import { ChatControllerStorage } from "./storage";
67
69
  import { listUnreadInboxItems } from "../unread-inbox";
70
+ import type { ChatConversationList } from "../conversations";
68
71
  import {
69
72
  applySignedOutChatControllerSession,
70
73
  createChatControllerSessionState,
@@ -75,6 +78,26 @@ import {
75
78
  const chatLog = debugLog.createLogger("chat-controller");
76
79
  const IMAGE_LINK_REFRESH_INTERVAL_MS = 60_000;
77
80
 
81
+ function timeOf(value: string | undefined): number {
82
+ const time = value ? Date.parse(value) : Number.NaN;
83
+ return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
84
+ }
85
+
86
+ /**
87
+ * The newest message held for a conversation, sent or received. A DM or group
88
+ * with nothing on this device yet counts from when it was started.
89
+ */
90
+ function lastConversationActivity(channel: ChatChannel, state: ChannelRuntimeState | undefined): number {
91
+ const started = channel.kind === "direct" || channel.kind === "group"
92
+ ? timeOf(channel.created_at)
93
+ : Number.NEGATIVE_INFINITY;
94
+ return Math.max(
95
+ started,
96
+ timeOf(state?.messages.at(-1)?.createdAt),
97
+ timeOf(state?.pendingMessages.at(-1)?.createdAt),
98
+ );
99
+ }
100
+
78
101
  export type { ChatControllerSnapshot } from "./state";
79
102
 
80
103
  export class ChatController {
@@ -186,6 +209,27 @@ export class ChatController {
186
209
  });
187
210
  }
188
211
 
212
+ /**
213
+ * Every conversation the Chat sidebar lists, with its unread count and last
214
+ * activity, from what this device already holds; no request. Null until a
215
+ * verified account is signed in.
216
+ */
217
+ listConversations(): ChatConversationList | null {
218
+ const user = this.session.user;
219
+ if (!user?.emailVerified) return null;
220
+ return {
221
+ userId: user.id,
222
+ states: this.channelCatalog.getChannels().map((channel) => {
223
+ const state = this.storage.channelStates.get(channel.id);
224
+ return {
225
+ channel,
226
+ unreadCount: Math.max(0, state?.unreadCount ?? 0),
227
+ lastActivityAt: lastConversationActivity(channel, state),
228
+ };
229
+ }),
230
+ };
231
+ }
232
+
189
233
  getChannels(): ChatChannel[] {
190
234
  return this.channelCatalog.getChannels();
191
235
  }
@@ -558,6 +602,10 @@ export class ChatController {
558
602
  this.notifyFn({ body: "Only your latest sent message can be edited.", type: "error" });
559
603
  return false;
560
604
  }
605
+ if (!isEditableOnGloom(latestOwnMessage)) {
606
+ this.notifyFn({ body: DISCORD_MESSAGE_EDIT_NOTICE, type: "error" });
607
+ return false;
608
+ }
561
609
  if (!isWithinChatMessageEditWindow(latestOwnMessage)) {
562
610
  this.notifyFn({ body: `Messages can only be edited within ${CHAT_MESSAGE_EDIT_WINDOW_LABEL}.`, type: "error" });
563
611
  return false;
@@ -0,0 +1,256 @@
1
+ import type { ChatChannel, ChatUserSummary } from "../../../api-client";
2
+ import { teamStore } from "../cloud/team/store";
3
+ import { formatChatPaneTitle } from "./channel-labels";
4
+
5
+ /** What the chat controller holds about one conversation, read without a request. */
6
+ export interface ChatConversationState {
7
+ channel: ChatChannel;
8
+ unreadCount: number;
9
+ /** Newest message on this device, or when a DM started; -Infinity when unknown. */
10
+ lastActivityAt: number;
11
+ }
12
+
13
+ /** The signed-in account and its conversations, as the chat controller holds them. */
14
+ export interface ChatConversationList {
15
+ userId: string;
16
+ states: ChatConversationState[];
17
+ }
18
+
19
+ export type ChatConversationKind = "channel" | "team" | "direct" | "group";
20
+
21
+ export interface ChatConversation {
22
+ channelId: string;
23
+ kind: ChatConversationKind;
24
+ /** As the Chat pane titles it: #general, MD·#general, @alice, a group's name. */
25
+ title: string;
26
+ /** Muted beside the title: a DM's full name, a group's members, a channel's team. */
27
+ name: string | null;
28
+ /** The other person's username in a DM, lower case. */
29
+ username: string | null;
30
+ unreadCount: number;
31
+ lastActivityAt: number;
32
+ /** Position in the Chat sidebar: channels, then team channels, then DMs. */
33
+ order: number;
34
+ /** What the conversation is called, lower case: channel, username, full name, group name. */
35
+ names: string[];
36
+ /** Every word a typed word may start, lower case. */
37
+ words: string[];
38
+ }
39
+
40
+ /** The empty bar is a browse list of everything; chat gets a few rows of it. */
41
+ const EMPTY_QUERY_CONVERSATION_LIMIT = 4;
42
+ /** Typing "messages" or "chat" asks for the list itself, so it gets more. */
43
+ const LISTING_CONVERSATION_LIMIT = 15;
44
+ const SEARCH_CONVERSATION_LIMIT = 8;
45
+ /** Below this many characters a match says little, so unread conversations lead. */
46
+ const SHORT_QUERY_LENGTH = 3;
47
+
48
+ /** Words that, typed alone, ask for the conversations rather than one of them. */
49
+ const LISTING_WORDS = new Set([
50
+ "chat",
51
+ "chats",
52
+ "message",
53
+ "messages",
54
+ "conversation",
55
+ "conversations",
56
+ "dm",
57
+ "dms",
58
+ ]);
59
+
60
+ const WORD = /[\p{L}\p{N}]+/gu;
61
+
62
+ function wordsOf(value: string | null | undefined): string[] {
63
+ return value?.toLowerCase().match(WORD) ?? [];
64
+ }
65
+
66
+ /** "Alice Smith" and "alice smith" are the same name. */
67
+ function nameKey(value: string | null | undefined): string | null {
68
+ const words = wordsOf(value);
69
+ return words.length > 0 ? words.join(" ") : null;
70
+ }
71
+
72
+ function conversationKind(channel: ChatChannel): ChatConversationKind {
73
+ if (channel.kind === "direct" || channel.kind === "group" || channel.kind === "team") return channel.kind;
74
+ return "channel";
75
+ }
76
+
77
+ const KIND_ORDER: Record<ChatConversationKind, number> = { channel: 0, team: 1, direct: 2, group: 2 };
78
+
79
+ function memberLabel(member: ChatUserSummary): string | null {
80
+ return member.username ? `@${member.username}` : member.displayName?.trim() || null;
81
+ }
82
+
83
+ function groupMembers(channel: ChatChannel, userId: string | null): string | null {
84
+ const members = (channel.members ?? []).filter((member) => member.id !== userId);
85
+ const labels = members.map(memberLabel).filter((label): label is string => !!label);
86
+ if (labels.length === 0) return null;
87
+ const shown = labels.slice(0, 3);
88
+ return labels.length > shown.length ? `${shown.join(", ")} +${labels.length - shown.length}` : shown.join(", ");
89
+ }
90
+
91
+ function describe(
92
+ channel: ChatChannel,
93
+ userId: string | null,
94
+ ): Pick<ChatConversation, "name" | "names" | "words"> & { username?: string | null } {
95
+ switch (conversationKind(channel)) {
96
+ case "direct": {
97
+ const username = channel.dmUser?.username?.trim() || null;
98
+ const displayName = channel.dmUser?.displayName?.trim() || null;
99
+ return {
100
+ // The title is @username when there is one; the full name says who that is.
101
+ name: username && displayName && nameKey(displayName) !== nameKey(username) ? displayName : null,
102
+ username: username?.replace(/^@+/, "").toLowerCase() ?? null,
103
+ names: [nameKey(username), nameKey(displayName)].filter((name): name is string => !!name),
104
+ words: [...wordsOf(username), ...wordsOf(displayName)],
105
+ };
106
+ }
107
+ case "group": {
108
+ const members = (channel.members ?? []).filter((member) => member.id !== userId);
109
+ return {
110
+ name: groupMembers(channel, userId),
111
+ names: [nameKey(channel.name)].filter((name): name is string => !!name),
112
+ words: [
113
+ ...wordsOf(channel.name),
114
+ ...members.flatMap((member) => [...wordsOf(member.username), ...wordsOf(member.displayName)]),
115
+ ],
116
+ };
117
+ }
118
+ case "team": {
119
+ const team = teamStore.getTeam(channel.teamId ?? null);
120
+ return {
121
+ name: team?.name ?? null,
122
+ names: [nameKey(channel.name)].filter((name): name is string => !!name),
123
+ words: [
124
+ ...wordsOf(channel.name),
125
+ ...wordsOf(team?.name),
126
+ ...wordsOf(team?.shortName),
127
+ ],
128
+ };
129
+ }
130
+ default:
131
+ return {
132
+ name: null,
133
+ names: [nameKey(channel.name)].filter((name): name is string => !!name),
134
+ words: wordsOf(channel.name),
135
+ };
136
+ }
137
+ }
138
+
139
+ /** The conversations the Chat sidebar lists, in its order, each named as the Chat pane names it. */
140
+ export function buildChatConversations(
141
+ states: readonly ChatConversationState[],
142
+ userId: string | null = null,
143
+ ): ChatConversation[] {
144
+ return states
145
+ .map((state, index): ChatConversation => {
146
+ const kind = conversationKind(state.channel);
147
+ const { username = null, ...described } = describe(state.channel, userId);
148
+ return {
149
+ channelId: state.channel.id,
150
+ kind,
151
+ title: formatChatPaneTitle(state.channel, state.channel.id),
152
+ username,
153
+ unreadCount: state.unreadCount,
154
+ lastActivityAt: state.lastActivityAt,
155
+ order: KIND_ORDER[kind] * 100_000 + index,
156
+ ...described,
157
+ };
158
+ })
159
+ .sort((left, right) => left.order - right.order);
160
+ }
161
+
162
+ function isConversationWithPeople(conversation: ChatConversation): boolean {
163
+ return conversation.kind === "direct" || conversation.kind === "group";
164
+ }
165
+
166
+ function compareUnreadFirst(left: ChatConversation, right: ChatConversation): number {
167
+ return Number(right.unreadCount > 0) - Number(left.unreadCount > 0);
168
+ }
169
+
170
+ /** Newest activity first; two with nothing known tie. */
171
+ function compareActivity(left: ChatConversation, right: ChatConversation): number {
172
+ const activity = right.lastActivityAt - left.lastActivityAt;
173
+ return Number.isNaN(activity) ? 0 : activity;
174
+ }
175
+
176
+ /** Newest activity first; the sidebar's order breaks ties. */
177
+ function compareRecent(left: ChatConversation, right: ChatConversation): number {
178
+ return compareActivity(left, right) || left.order - right.order;
179
+ }
180
+
181
+ /**
182
+ * Unread conversations first, newest then most unread; then DMs and groups by
183
+ * their last activity; then channels in the sidebar's order.
184
+ */
185
+ function compareListing(left: ChatConversation, right: ChatConversation): number {
186
+ const unread = compareUnreadFirst(left, right);
187
+ if (unread !== 0) return unread;
188
+ if (left.unreadCount > 0) {
189
+ return compareActivity(left, right) || right.unreadCount - left.unreadCount || left.order - right.order;
190
+ }
191
+ const people = Number(isConversationWithPeople(right)) - Number(isConversationWithPeople(left));
192
+ if (people !== 0) return people;
193
+ return isConversationWithPeople(left) ? compareRecent(left, right) : left.order - right.order;
194
+ }
195
+
196
+ /**
197
+ * How well typed words name a conversation: 3 when they are its name, 2 when
198
+ * its name starts with them, 1 when each starts a word of it, 0 otherwise.
199
+ */
200
+ function rankChatConversation(conversation: ChatConversation, query: string): number {
201
+ const tokens = wordsOf(query);
202
+ if (tokens.length === 0) return 0;
203
+ const phrase = tokens.join(" ");
204
+ if (conversation.names.includes(phrase)) return 3;
205
+ if (conversation.names.some((name) => name.startsWith(phrase))) return 2;
206
+ return tokens.every((token) => conversation.words.some((word) => word.startsWith(token))) ? 1 : 0;
207
+ }
208
+
209
+ export type ChatConversationQuery =
210
+ | { kind: "recent" }
211
+ | { kind: "listing" }
212
+ | { kind: "search"; text: string };
213
+
214
+ /**
215
+ * What the typed text asks for: nothing typed is the recent list, a word like
216
+ * "messages" or "chat" on its own is the whole list, and "chat gen" searches
217
+ * for "gen".
218
+ */
219
+ export function parseChatConversationQuery(query: string): ChatConversationQuery {
220
+ const trimmed = query.trim();
221
+ if (!trimmed) return { kind: "recent" };
222
+ const [first = "", ...rest] = trimmed.split(/\s+/);
223
+ if (!LISTING_WORDS.has(first.toLowerCase())) return { kind: "search", text: trimmed };
224
+ return rest.length === 0 ? { kind: "listing" } : { kind: "search", text: rest.join(" ") };
225
+ }
226
+
227
+ /**
228
+ * The conversations that answer the text in the command bar, best first.
229
+ * With nothing typed: unread ones and the latest DMs, a few at most. Short
230
+ * text puts unread matches first; longer text the closest names.
231
+ */
232
+ export function selectChatConversations(
233
+ conversations: readonly ChatConversation[],
234
+ query: ChatConversationQuery,
235
+ ): ChatConversation[] {
236
+ if (query.kind === "recent") {
237
+ return conversations
238
+ .filter((conversation) => conversation.unreadCount > 0 || isConversationWithPeople(conversation))
239
+ .sort(compareListing)
240
+ .slice(0, EMPTY_QUERY_CONVERSATION_LIMIT);
241
+ }
242
+ if (query.kind === "listing") {
243
+ return [...conversations].sort(compareListing).slice(0, LISTING_CONVERSATION_LIMIT);
244
+ }
245
+ const shortQuery = query.text.replace(/[#@\s]/g, "").length < SHORT_QUERY_LENGTH;
246
+ return conversations
247
+ .map((conversation) => ({ conversation, rank: rankChatConversation(conversation, query.text) }))
248
+ .filter((entry) => entry.rank > 0)
249
+ .sort((left, right) => {
250
+ const unread = compareUnreadFirst(left.conversation, right.conversation);
251
+ if (shortQuery && unread !== 0) return unread;
252
+ return right.rank - left.rank || unread || compareRecent(left.conversation, right.conversation);
253
+ })
254
+ .slice(0, SEARCH_CONVERSATION_LIMIT)
255
+ .map((entry) => entry.conversation);
256
+ }
@@ -0,0 +1,91 @@
1
+ import { apiClient, type ChatDiscordLink } from "../../../api-client";
2
+ import { ApiRequestError } from "../../../api-client/errors";
3
+ import type { AppNotificationRequest } from "../../../types/plugin";
4
+ import type { DiscordComposerCommand } from "./composer-commands";
5
+
6
+ /**
7
+ * Where /discord sends someone to connect. The approval has to happen in a
8
+ * browser that is signed in to gloom.sh as the same account, which a terminal
9
+ * user's often is not, so the page handles the web sign-in and then starts the
10
+ * Discord authorization itself.
11
+ */
12
+ export const DISCORD_LINK_URL = "https://gloom.sh/discord/link";
13
+
14
+ /** The Discord calls the composer's /discord command makes. */
15
+ export interface DiscordCommandApi {
16
+ getLink(): Promise<ChatDiscordLink>;
17
+ unlink(): Promise<void>;
18
+ setMirror(enabled: boolean): Promise<void>;
19
+ }
20
+
21
+ const discordCommandApi: DiscordCommandApi = {
22
+ getLink: () => apiClient.getChatDiscordLink(),
23
+ unlink: () => apiClient.unlinkChatDiscord(),
24
+ setMirror: (enabled) => apiClient.setChatDiscordMirror(enabled),
25
+ };
26
+
27
+ export const DISCORD_UNAVAILABLE = "Discord sync is not available yet";
28
+ const DISCORD_USAGE = "Use /discord to connect, /discord unlink, or /discord mirror on|off";
29
+
30
+ type DiscordCommandOutcome = Pick<AppNotificationRequest, "body" | "type">;
31
+
32
+ /** A server without Discord sync answers its routes with 404, or 501 where it knows them but has no bridge. */
33
+ function isUnavailable(error: unknown): boolean {
34
+ return error instanceof ApiRequestError && (error.status === 404 || error.status === 501);
35
+ }
36
+
37
+ function linkedStatus(link: ChatDiscordLink): string {
38
+ const name = link.discordUsername ? `Linked as ${link.discordUsername}` : "Linked to Discord";
39
+ const mirror = link.mirror ? "Mirroring is on" : "Mirroring is off";
40
+ return `${name}. ${mirror}. /discord mirror on|off, /discord unlink`;
41
+ }
42
+
43
+ const UNAVAILABLE_OUTCOME: DiscordCommandOutcome = { body: DISCORD_UNAVAILABLE, type: "info" };
44
+
45
+ async function connect(api: DiscordCommandApi, openUrl: (url: string) => Promise<void>): Promise<DiscordCommandOutcome> {
46
+ const link = await api.getLink();
47
+ if (!link.available) return UNAVAILABLE_OUTCOME;
48
+ if (link.linked) return { body: linkedStatus(link), type: "info" };
49
+ await openUrl(DISCORD_LINK_URL);
50
+ return { body: "Opened gloom.sh in your browser to connect Discord", type: "info" };
51
+ }
52
+
53
+ async function unlink(api: DiscordCommandApi): Promise<DiscordCommandOutcome> {
54
+ // A 404 from the unlink route alone could mean "no Discord sync" or "nothing linked", so ask first.
55
+ const link = await api.getLink();
56
+ if (!link.available) return UNAVAILABLE_OUTCOME;
57
+ if (!link.linked) return { body: "Discord is not linked", type: "info" };
58
+ await api.unlink();
59
+ return { body: "Unlinked from Discord", type: "success" };
60
+ }
61
+
62
+ /**
63
+ * Runs one /discord subcommand and says what to tell the user. A server that
64
+ * has no Discord sync answers 404 or 501, which reads as "not available yet"
65
+ * and nothing else.
66
+ */
67
+ export async function runDiscordCommand(
68
+ command: DiscordComposerCommand,
69
+ deps: { api?: DiscordCommandApi; openUrl: (url: string) => Promise<void> },
70
+ ): Promise<DiscordCommandOutcome> {
71
+ const api = deps.api ?? discordCommandApi;
72
+ try {
73
+ switch (command.action) {
74
+ case "connect":
75
+ return await connect(api, deps.openUrl);
76
+ case "unlink":
77
+ return await unlink(api);
78
+ case "mirror":
79
+ await api.setMirror(command.enabled);
80
+ return {
81
+ body: command.enabled ? "Your public messages are mirrored to Discord" : "Your public messages are no longer mirrored to Discord",
82
+ type: "success",
83
+ };
84
+ case "usage":
85
+ return { body: DISCORD_USAGE, type: "info" };
86
+ }
87
+ } catch (error) {
88
+ if (isUnavailable(error)) return UNAVAILABLE_OUTCOME;
89
+ return { body: error instanceof Error && error.message ? error.message : "Discord request failed.", type: "error" };
90
+ }
91
+ }
@@ -8,17 +8,23 @@ export function isWithinChatMessageEditWindow(message: ChatMessage, nowMs = Date
8
8
  return Number.isFinite(createdMs) && nowMs - createdMs <= CHAT_MESSAGE_EDIT_WINDOW_MS;
9
9
  }
10
10
 
11
+ /** What the server answers (409) to an edit of a message written on Discord. */
12
+ export const DISCORD_MESSAGE_EDIT_NOTICE = "This message was sent on Discord. Edit it there.";
13
+
14
+ /** A message written on Discord is edited there; the server refuses an edit of it. */
15
+ export function isEditableOnGloom(message: ChatMessage): boolean {
16
+ return message.origin !== "discord";
17
+ }
18
+
11
19
  export function findLatestEditableChatMessage(
12
20
  messages: ChatMessage[],
13
21
  userId: string | null | undefined,
14
22
  nowMs = Date.now(),
15
23
  ): ChatMessage | null {
16
24
  if (!userId) return null;
17
- return [...messages]
25
+ // Only your latest message can be edited, so a Discord one in that place leaves nothing to edit.
26
+ const latest = [...messages]
18
27
  .reverse()
19
- .find((message) => (
20
- message.user.id === userId
21
- && !message.clientStatus
22
- && isWithinChatMessageEditWindow(message, nowMs)
23
- )) ?? null;
28
+ .find((message) => message.user.id === userId && !message.clientStatus);
29
+ return latest && isEditableOnGloom(latest) && isWithinChatMessageEditWindow(latest, nowMs) ? latest : null;
24
30
  }
@@ -0,0 +1,14 @@
1
+ import type { ChatUserSummary } from "../../../api-client";
2
+
3
+ /**
4
+ * A person on the Gloom Discord with no linked Gloom account. Their messages
5
+ * show in the channel, but there is no profile to open and no one to message.
6
+ */
7
+ export function isDiscordGhost(user: Pick<ChatUserSummary, "accountType"> | null | undefined): boolean {
8
+ return user?.accountType === "discord";
9
+ }
10
+
11
+ /** The name a message is signed with: the username, or for a ghost that has none the Discord name it carries. */
12
+ export function chatAuthorName(user: Pick<ChatUserSummary, "username" | "displayName" | "accountType">): string {
13
+ return user.username ?? ((isDiscordGhost(user) && user.displayName?.trim()) || "anon");
14
+ }
@@ -10,13 +10,33 @@ import {
10
10
  openDmTargetFromCommand,
11
11
  parseDmUsernames,
12
12
  } from "./channels";
13
+ import { CHAT_REUSE_PANE_VALUE, createChatSearchProvider } from "./command-bar";
13
14
  import { chatController } from "./controller";
15
+ import { buildChatConversations, selectChatConversations } from "./conversations";
16
+ import { pickChatPaneToReuse } from "./pane-state";
14
17
  import { ChatPane } from "./pane";
15
18
  import { chatSidebarStore } from "./sidebar-store";
16
19
  import { ChatStatusWidget } from "./status-widget";
17
20
  import { UnreadInboxPane } from "./unread-inbox-pane";
18
21
  import { UNREAD_INBOX_PANE_ID, UNREAD_INBOX_TEMPLATE_ID } from "./unread-inbox";
19
22
 
23
+ /**
24
+ * `CHAT general` or `CHAT #help` by id; failing that, `CHAT gen` or `CHAT alice`
25
+ * opens the conversation the command bar lists first for that text.
26
+ */
27
+ async function resolveTypedChannelId(rawArg: string): Promise<string> {
28
+ try {
29
+ return await chatController.resolveRequiredChannelId(normalizeShortcutChannelId(rawArg));
30
+ } catch (error) {
31
+ const list = chatController.listConversations();
32
+ const [best] = list
33
+ ? selectChatConversations(buildChatConversations(list.states, list.userId), { kind: "search", text: rawArg })
34
+ : [];
35
+ if (best) return best.channelId;
36
+ throw error;
37
+ }
38
+ }
39
+
20
40
  export const chatModule: PluginModule = {
21
41
  panes: [{
22
42
  id: "chat",
@@ -47,21 +67,38 @@ export const chatModule: PluginModule = {
47
67
  keywords: ["new", "chat", "pane", "message"],
48
68
  shortcut: { prefix: "CHAT", argPlaceholder: "channel", argKind: "text" },
49
69
  createInstance: async (context, options) => {
70
+ const reuse = options?.values?.[CHAT_REUSE_PANE_VALUE] === "1";
71
+ const rawArg = options?.arg?.trim() ?? "";
72
+ // The command bar's New DM keeps the open pane on its conversation.
73
+ if (reuse && !rawArg) {
74
+ const open = pickChatPaneToReuse(context.layout, context.focusedPaneId, null);
75
+ if (open) return { placement: "floating", instanceId: open.instanceId };
76
+ }
50
77
  // `CHAT MD` or `CHAT "Macro Desk"` opens that team's #general. A raw
51
78
  // channel id (team ids are mixed case) is kept as typed.
52
- const rawArg = options?.arg?.trim() ?? "";
53
79
  const team = rawArg ? teamStore.findTeam(rawArg) : null;
54
80
  const channelId = team
55
81
  ? teamChannelId(team.id)
56
82
  : rawArg && chatController.getChannels().some((entry) => entry.id === rawArg)
57
83
  ? rawArg
58
84
  : rawArg
59
- ? await chatController.resolveRequiredChannelId(normalizeShortcutChannelId(rawArg))
85
+ ? await resolveTypedChannelId(rawArg)
60
86
  : await chatController.resolvePreferredChannelId(
61
87
  getPreferredChatOpenChannelId(context.config, chatController.getSnapshot()),
62
88
  );
63
89
  const channel = chatController.getChannels().find((entry) => entry.id === channelId);
64
90
  const targetMessageId = options?.values?.messageId?.trim() || null;
91
+ // A conversation picked in the command bar goes to the Chat pane that is
92
+ // open, which switches to it, rather than to a pane of its own.
93
+ const open = reuse ? pickChatPaneToReuse(context.layout, context.focusedPaneId, channelId) : null;
94
+ if (open) {
95
+ return {
96
+ placement: "floating",
97
+ instanceId: open.instanceId,
98
+ title: formatChatPaneTitle(channel, channelId),
99
+ settings: { channelId },
100
+ };
101
+ }
65
102
  return {
66
103
  placement: "floating",
67
104
  // One pane per channel: re-opening the same channel focuses the pane
@@ -93,6 +130,7 @@ export const chatModule: PluginModule = {
93
130
  "status:widget": () => <ChatStatusWidget />,
94
131
  },
95
132
  setup(ctx) {
133
+ ctx.registerCommandBarSearchProvider(createChatSearchProvider(ctx));
96
134
  chatController.attachPersistence(ctx.persistence, ctx.resume);
97
135
  chatSidebarStore.attach(ctx.persistence);
98
136
  chatController.setNotifier(ctx.notify, (channelId, messageId) => {
@@ -23,6 +23,8 @@ export function isGroupedWithPrevious(messages: ChatMessage[], index: number) {
23
23
  const prev = messages[index - 1]!;
24
24
  const curr = messages[index]!;
25
25
  if (prev.user.id !== curr.user.id) return false;
26
+ // The Discord tag sits on a header, so a linked person's Discord and app messages do not share one.
27
+ if ((prev.origin === "discord") !== (curr.origin === "discord")) return false;
26
28
  return new Date(curr.createdAt).getTime() - new Date(prev.createdAt).getTime() < MESSAGE_GROUP_THRESHOLD_MS;
27
29
  }
28
30
 
@@ -7,6 +7,7 @@ import { useAppLanguage } from "../../../../i18n/react";
7
7
  import { normalizeInlinePreview } from "../layout";
8
8
  import { DesktopChatMessageImages } from "../attachments/desktop";
9
9
  import { chatReplyQuoteText } from "../attachments/model";
10
+ import { chatAuthorName } from "../ghost-user";
10
11
  import { ChatMessageActions, ChatMessageHeader } from "./header";
11
12
  import { ResponsiveTickerBadgeText } from "./inline-tokens";
12
13
  import { useSlowPendingSend } from "./pending-send";
@@ -153,7 +154,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
153
154
  <DesktopChatMessageImages
154
155
  attachments={attachments}
155
156
  caption={msg.content}
156
- author={msg.user.username ?? "anon"}
157
+ author={chatAuthorName(msg.user)}
157
158
  review={msg.attachmentReview}
158
159
  dimmed={state.isSending || msg.clientStatus === "failed"}
159
160
  onLoadError={onImageLoadError}
@@ -1,6 +1,8 @@
1
1
  import { useRef } from "react";
2
2
  import { Box, Text, useUiCapabilities, type BoxRenderable } from "../../../../ui";
3
3
  import { t } from "../../../../i18n";
4
+ import { displayWidth } from "../../../../utils/format";
5
+ import { chatAuthorName, isDiscordGhost } from "../ghost-user";
4
6
  import { Button } from "../../../../components/ui";
5
7
  import { MESSAGE_ACTION_WIDTH } from "../layout";
6
8
  import type { ChatMessageRenderState } from "./render-state";
@@ -82,7 +84,9 @@ export function ChatMessageHeader({
82
84
  /** The terminal sizes the author cell to its label. */
83
85
  fitAuthorWidth?: boolean;
84
86
  }) {
85
- const authorLabel = msg.user.username ?? "anon";
87
+ const authorLabel = chatAuthorName(msg.user);
88
+ // A Discord ghost has no Gloom profile to open and no one to message.
89
+ const hasProfile = !isDiscordGhost(msg.user);
86
90
  const { nativeContextMenu } = useUiCapabilities();
87
91
  // The card opens beside the name the pointer is on.
88
92
  const nameRef = useRef<BoxRenderable | null>(null);
@@ -90,26 +94,31 @@ export function ChatMessageHeader({
90
94
  <Box {...rowProps} flexDirection="row" height={1} paddingLeft={1}>
91
95
  <Box
92
96
  ref={nameRef}
93
- width={fitAuthorWidth ? authorLabel.length : undefined}
97
+ width={fitAuthorWidth ? displayWidth(authorLabel) : undefined}
94
98
  height={1}
95
- onMouseOver={() => onUserHover(msg.user, nameRef.current)}
96
- onMouseMove={() => onUserHover(msg.user, nameRef.current)}
97
- onMouseOut={onUserHoverEnd}
98
- data-gloom-context-menu-surface="true"
99
- onMouseDown={(event: { button?: number; preventDefault?: () => void; stopPropagation?: () => void }) => {
100
- event.preventDefault?.();
101
- event.stopPropagation?.();
102
- // A native menu opens on the right-click's contextmenu event instead.
103
- if (event.button === 2 && nativeContextMenu === true) return;
104
- onUserActivate?.(msg.user, nameRef.current);
105
- }}
106
- onContextMenu={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => onUserContextMenu?.(msg.user, event, nameRef.current)}
107
- style={{ cursor: "pointer" }}
99
+ {...(hasProfile
100
+ ? {
101
+ onMouseOver: () => onUserHover(msg.user, nameRef.current),
102
+ onMouseMove: () => onUserHover(msg.user, nameRef.current),
103
+ onMouseOut: onUserHoverEnd,
104
+ "data-gloom-context-menu-surface": "true",
105
+ onMouseDown: (event: { button?: number; preventDefault?: () => void; stopPropagation?: () => void }) => {
106
+ event.preventDefault?.();
107
+ event.stopPropagation?.();
108
+ // A native menu opens on the right-click's contextmenu event instead.
109
+ if (event.button === 2 && nativeContextMenu === true) return;
110
+ onUserActivate?.(msg.user, nameRef.current);
111
+ },
112
+ onContextMenu: (event: { preventDefault?: () => void; stopPropagation?: () => void }) => onUserContextMenu?.(msg.user, event, nameRef.current),
113
+ style: { cursor: "pointer" },
114
+ }
115
+ : {})}
108
116
  >
109
117
  <Text fg={state.authorColor} attributes={state.authorAttributes}>
110
118
  {authorLabel}
111
119
  </Text>
112
120
  </Box>
121
+ {msg.origin === "discord" && <Text fg={state.originTagColor}> Discord</Text>}
113
122
  <Text fg={state.headerStatusColor}> {state.headerStatus}</Text>
114
123
  {(state.showReplyAction || (state.showRetryAction && actionProps.retryMessage)) && <Text fg={state.headerStatusColor}> </Text>}
115
124
  <ChatMessageActions state={state} {...actionProps} />
@@ -5,6 +5,7 @@ import { colors } from "../../../../theme/colors";
5
5
  import type { ChatUserSummary, PublicPortfolioAnalytics } from "../../../../api-client";
6
6
  import { displayWidth, formatNumber, truncateToDisplayWidth } from "../../../../utils/format";
7
7
  import { truncateWithEllipsis } from "../../../../utils/text-wrap";
8
+ import { isDiscordGhost } from "../ghost-user";
8
9
  import type { ChatUserAnchor } from "./types";
9
10
 
10
11
  /**
@@ -24,7 +25,7 @@ function hasPortfolioAnalytics(analytics: PublicPortfolioAnalytics | null | unde
24
25
  }
25
26
 
26
27
  export function hasPublicChatProfileInfo(user: ChatUserSummary): boolean {
27
- if (user.profilePublic === false) return false;
28
+ if (user.profilePublic === false || isDiscordGhost(user)) return false;
28
29
  return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim() || hasPortfolioAnalytics(user.portfolioAnalytics));
29
30
  }
30
31