gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -1,36 +1,140 @@
1
1
  import { Box, Text } from "../../../../ui";
2
2
  import { TextAttributes } from "../../../../ui";
3
3
  import { colors } from "../../../../theme/colors";
4
- import type { ChatUserSummary } from "../../../../api-client";
4
+ import type { ChatUserSummary, PublicPortfolioAnalytics } from "../../../../api-client";
5
+ import { formatNumber } from "../../../../utils/format";
5
6
  import { truncateChannelLabel } from "../channels";
6
- import { ChatActionChip } from "./action-chip";
7
7
 
8
8
  export const PROFILE_POPOVER_CLOSE_DELAY_MS = 40;
9
9
 
10
+ function hasPortfolioAnalytics(analytics: PublicPortfolioAnalytics | null | undefined): boolean {
11
+ return Boolean(
12
+ analytics
13
+ && (
14
+ analytics.oneYearReturn != null
15
+ || analytics.spyBeta != null
16
+ ),
17
+ );
18
+ }
19
+
10
20
  export function hasPublicChatProfileInfo(user: ChatUserSummary): boolean {
11
21
  if (user.profilePublic === false) return false;
12
- return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim());
22
+ return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim() || hasPortfolioAnalytics(user.portfolioAnalytics));
23
+ }
24
+
25
+ function formatSignedPercent(value: number): string {
26
+ const percent = value * 100;
27
+ return `${percent >= 0 ? "+" : ""}${formatNumber(percent, 2)}%`;
28
+ }
29
+
30
+ type AnalyticsMetric = {
31
+ id: "one-year" | "beta";
32
+ label: string;
33
+ value: string;
34
+ rawValue: number;
35
+ };
36
+
37
+ function analyticsValueColor(id: string, value: number): string {
38
+ if (id === "one-year") {
39
+ if (value > 0) return colors.positive;
40
+ if (value < 0) return colors.negative;
41
+ }
42
+ return colors.warning;
43
+ }
44
+
45
+ function analyticsMetrics(analytics: PublicPortfolioAnalytics): AnalyticsMetric[] {
46
+ return [
47
+ analytics.oneYearReturn != null
48
+ ? {
49
+ id: "one-year",
50
+ label: "1Y",
51
+ value: formatSignedPercent(analytics.oneYearReturn),
52
+ rawValue: analytics.oneYearReturn,
53
+ }
54
+ : null,
55
+ analytics.spyBeta != null
56
+ ? {
57
+ id: "beta",
58
+ label: "Beta",
59
+ value: formatNumber(analytics.spyBeta, 2),
60
+ rawValue: analytics.spyBeta,
61
+ }
62
+ : null,
63
+ ].filter((metric): metric is AnalyticsMetric => !!metric);
64
+ }
65
+
66
+ function headerMetricLabel(metric: AnalyticsMetric): string {
67
+ return metric.id === "beta" ? "Beta" : metric.label;
68
+ }
69
+
70
+ function headerMetricsNaturalWidth(metrics: AnalyticsMetric[]): number {
71
+ const metricWidth = metrics.reduce((sum, metric) => (
72
+ sum + headerMetricLabel(metric).length + 1 + metric.value.length
73
+ ), 0);
74
+ return metricWidth + Math.max(0, metrics.length - 1);
75
+ }
76
+
77
+ function HeaderAnalyticsStats({
78
+ metrics,
79
+ width,
80
+ }: {
81
+ metrics: AnalyticsMetric[];
82
+ width: number;
83
+ }) {
84
+ if (metrics.length === 0 || width < 8) return null;
85
+ const gapWidth = Math.max(0, metrics.length - 1);
86
+ const naturalWidths = metrics.map((metric) => headerMetricLabel(metric).length + 1 + metric.value.length);
87
+ const availableMetricWidth = Math.max(metrics.length * 4, width - gapWidth);
88
+ let overflow = Math.max(0, naturalWidths.reduce((sum, value) => sum + value, 0) - availableMetricWidth);
89
+ const metricWidths = naturalWidths.map((naturalWidth, index) => {
90
+ const shrinkable = Math.max(0, naturalWidth - 4);
91
+ const shrink = Math.min(shrinkable, Math.ceil(overflow / (naturalWidths.length - index)));
92
+ overflow -= shrink;
93
+ return naturalWidth - shrink;
94
+ });
95
+
96
+ return (
97
+ <Box flexDirection="row" gap={1} width={width}>
98
+ {metrics.map((metric, index) => {
99
+ const label = headerMetricLabel(metric);
100
+ const metricWidth = metricWidths[index] ?? 4;
101
+ const labelWidth = Math.max(1, Math.min(label.length, metricWidth - 2));
102
+ const valueWidth = Math.max(1, metricWidth - labelWidth - 1);
103
+ return (
104
+ <Box key={metric.id} width={metricWidth} height={1} flexDirection="row" gap={1}>
105
+ <Text fg={colors.textMuted}>{truncateChannelLabel(label, labelWidth)}</Text>
106
+ <Text fg={analyticsValueColor(metric.id, metric.rawValue)} attributes={TextAttributes.BOLD}>
107
+ {truncateChannelLabel(metric.value, valueWidth)}
108
+ </Text>
109
+ </Box>
110
+ );
111
+ })}
112
+ </Box>
113
+ );
13
114
  }
14
115
 
15
116
  export function UserProfilePopover({
16
117
  user,
17
118
  width,
18
- currentUserId,
19
- onDirectMessage,
20
119
  onClose,
21
120
  onKeepOpen,
22
121
  }: {
23
122
  user: ChatUserSummary;
24
123
  width: number;
25
- currentUserId?: string | null;
26
- onDirectMessage: (user: ChatUserSummary) => void;
27
124
  onClose: () => void;
28
125
  onKeepOpen: () => void;
29
126
  }) {
30
127
  const popoverWidth = Math.max(24, Math.min(38, width - 4));
31
128
  const meta = [user.title, user.company].filter(Boolean).join(" · ");
32
129
  const bio = user.bio?.trim();
33
- const canDm = user.id === currentUserId || user.acceptUnknownDms !== false;
130
+ const analytics = user.portfolioAnalytics;
131
+ const metrics = analytics ? analyticsMetrics(analytics) : [];
132
+ const headerWidth = Math.max(1, popoverWidth - 2);
133
+ const maxStatsWidth = Math.max(0, headerWidth - 8);
134
+ const statsWidth = metrics.length > 0 && maxStatsWidth >= 8
135
+ ? Math.min(headerMetricsNaturalWidth(metrics), maxStatsWidth)
136
+ : 0;
137
+ const usernameWidth = Math.max(1, headerWidth - (statsWidth > 0 ? statsWidth + 1 : 0));
34
138
 
35
139
  return (
36
140
  <Box
@@ -47,19 +151,19 @@ export function UserProfilePopover({
47
151
  onMouseOut={onClose}
48
152
  style={{ zIndex: 4 }}
49
153
  >
50
- <Box height={1} flexDirection="row">
51
- <Text fg={colors.positive} attributes={TextAttributes.BOLD}>
52
- {truncateChannelLabel(user.username ? `@${user.username}` : user.displayName, Math.max(popoverWidth - 10, 1))}
53
- </Text>
154
+ <Box height={1} width={headerWidth} flexDirection="row">
155
+ <Box width={usernameWidth}>
156
+ <Text fg={colors.positive} attributes={TextAttributes.BOLD}>
157
+ {truncateChannelLabel(user.username ? `@${user.username}` : user.displayName, usernameWidth)}
158
+ </Text>
159
+ </Box>
160
+ {statsWidth > 0 ? (
161
+ <>
162
+ <Box width={1} />
163
+ <HeaderAnalyticsStats metrics={metrics} width={statsWidth} />
164
+ </>
165
+ ) : null}
54
166
  <Box flexGrow={1} />
55
- <ChatActionChip
56
- label={canDm ? "DM" : "Closed"}
57
- width={canDm ? 5 : 9}
58
- emphasized
59
- onPress={() => {
60
- if (canDm) onDirectMessage(user);
61
- }}
62
- />
63
167
  </Box>
64
168
  {meta ? <Text fg={colors.textDim}>{truncateChannelLabel(meta, popoverWidth - 2)}</Text> : null}
65
169
  {bio ? (
@@ -5,9 +5,10 @@ import { AppContext, appReducer, createInitialState, PaneInstanceProvider } from
5
5
  import { createConfigBackedTestPluginRuntime, createTestPluginRuntime } from "../../../test-support/plugin-runtime";
6
6
  import { createDefaultConfig, findPaneInstance, type PaneInstanceConfig } from "../../../types/config";
7
7
  import { TextAttributes } from "../../../ui";
8
+ import { apiClient } from "../../../api-client";
8
9
  import { PluginRenderProvider } from "../../runtime";
9
10
  import { gloomberbCloudPlugin } from "../cloud";
10
- import { ChatContent } from "../chat";
11
+ import { ChatContent } from "./content";
11
12
  import { chatController } from "./controller";
12
13
  import { useChatChannelNavigation } from "./content/channel-navigation";
13
14
  import {
@@ -18,6 +19,7 @@ import {
18
19
  installChatApiTestDefaults,
19
20
  installServerChannels,
20
21
  lineText,
22
+ makeMessage,
21
23
  MemoryPersistence,
22
24
  type ChatTestSetup,
23
25
  } from "./test-harness";
@@ -38,7 +40,11 @@ afterEach(async () => {
38
40
  testSetup = undefined;
39
41
  });
40
42
 
41
- function createChannelPane(controller: ReturnType<typeof createController>, initialChannelId = "equities") {
43
+ function createChannelPane(
44
+ controller: ReturnType<typeof createController>,
45
+ initialChannelId = "equities",
46
+ onChannelChange?: (channelId: string) => void,
47
+ ) {
42
48
  const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
43
49
 
44
50
  return function ChannelPane() {
@@ -52,7 +58,10 @@ function createChannelPane(controller: ReturnType<typeof createController>, init
52
58
  height={12}
53
59
  focused
54
60
  channelId={channelId}
55
- onChannelChange={setChannelId}
61
+ onChannelChange={(nextChannelId) => {
62
+ onChannelChange?.(nextChannelId);
63
+ setChannelId(nextChannelId);
64
+ }}
56
65
  />
57
66
  </PluginRenderProvider>
58
67
  </AppContext>
@@ -214,6 +223,76 @@ describe("ChatContent channel sidebar", () => {
214
223
  expect(setup().captureCharFrame()).not.toContain("#options");
215
224
  });
216
225
 
226
+ test("opens a new direct-message dialog from the DMs header", async () => {
227
+ const controller = createController({
228
+ messages: [{
229
+ ...makeMessage(1),
230
+ user: { id: "u2", username: "bob", displayName: "Bob" },
231
+ }],
232
+ sessionToken: "token-123",
233
+ user: { id: "u1", username: "vince", emailVerified: true },
234
+ });
235
+ installServerChannels(controller);
236
+ controller.refreshChannels = async () => {};
237
+ controller.refreshChannelMessages = async () => {};
238
+ const openedTargets: Array<{ username?: string }> = [];
239
+ const originalOpenDirectChannel = apiClient.openDirectChannel.bind(apiClient);
240
+ apiClient.openDirectChannel = async (target) => {
241
+ openedTargets.push(target);
242
+ return {
243
+ id: "dm:bob",
244
+ name: "@bob",
245
+ kind: "direct",
246
+ created_at: "2026-07-03T09:30:00.000Z",
247
+ dmUser: { id: "u2", username: "bob", displayName: "Bob" },
248
+ };
249
+ };
250
+ const selectedChannels: string[] = [];
251
+ const ChannelPane = createChannelPane(controller, "everyone", (channelId) => {
252
+ selectedChannels.push(channelId);
253
+ });
254
+
255
+ try {
256
+ await act(async () => {
257
+ testSetup = await testRender(<ChannelPane />, {
258
+ width: 90,
259
+ height: 14,
260
+ });
261
+ });
262
+
263
+ await flushFrame();
264
+ const lines = setup().captureCharFrame().split("\n");
265
+ const row = lines.findIndex((line) => line.includes("DMs"));
266
+ const col = lines[row]?.lastIndexOf("+") ?? -1;
267
+ expect(row).toBeGreaterThanOrEqual(0);
268
+ expect(col).toBeGreaterThanOrEqual(0);
269
+
270
+ await act(async () => {
271
+ await setup().mockMouse.click(col, row);
272
+ await setup().renderOnce();
273
+ await setup().renderOnce();
274
+ });
275
+ await flushFrame();
276
+
277
+ expect(setup().captureCharFrame()).toContain("New DM");
278
+
279
+ await act(async () => {
280
+ await setup().mockInput.typeText("@bob");
281
+ setup().mockInput.pressEnter();
282
+ await setup().renderOnce();
283
+ await setup().renderOnce();
284
+ });
285
+ await flushFrame();
286
+
287
+ expect(openedTargets).toEqual([{ username: "bob" }]);
288
+ expect(selectedChannels).toEqual(["dm:bob"]);
289
+ expect(setup().captureCharFrame()).toContain("@bob");
290
+ expect(setup().captureCharFrame()).not.toContain("New DM");
291
+ } finally {
292
+ apiClient.openDirectChannel = originalOpenDirectChannel;
293
+ }
294
+ });
295
+
217
296
  test("shows the sidebar online count footer", async () => {
218
297
  const controller = createController({ sessionToken: "token-123" });
219
298
  installServerChannels(controller);
@@ -287,7 +366,6 @@ describe("ChatContent channel sidebar", () => {
287
366
  });
288
367
 
289
368
  test("coalesces rapid sidebar navigation to the final channel", async () => {
290
- const controller = createController({ sessionToken: "token-123" });
291
369
  const channels = [
292
370
  { id: "everyone", name: "everyone", created_at: "2026-03-26T12:10:05.684Z" },
293
371
  { id: "equities", name: "equities", created_at: "2026-05-09T00:00:00.000Z" },
@@ -314,8 +392,6 @@ describe("ChatContent channel sidebar", () => {
314
392
  channelIdRef,
315
393
  channels,
316
394
  channelsLoading: false,
317
- closeProfilePopover: () => {},
318
- controller,
319
395
  focused: true,
320
396
  inputFocused: false,
321
397
  onChannelChange: handleChannelChange,
@@ -412,6 +488,7 @@ describe("ChatContent channel sidebar", () => {
412
488
  },
413
489
  registerPaneTemplate: () => {},
414
490
  registerTickerResearchTab: () => {},
491
+ registerSyncTransport: () => {},
415
492
  registerShortcut: () => {},
416
493
  registerCommand: () => {},
417
494
  showPane: () => {},
@@ -109,9 +109,11 @@ export function ChannelSidebar({
109
109
  keyboardFocused,
110
110
  loading,
111
111
  canManageNotifications,
112
+ canCreateConversation,
112
113
  directExpanded,
113
114
  onSelect,
114
115
  onFocusRequest,
116
+ onCreateConversation,
115
117
  onToggleNotifications,
116
118
  onToggleDirectExpanded,
117
119
  }: {
@@ -125,9 +127,11 @@ export function ChannelSidebar({
125
127
  keyboardFocused: boolean;
126
128
  loading: boolean;
127
129
  canManageNotifications: boolean;
130
+ canCreateConversation: boolean;
128
131
  directExpanded: boolean;
129
132
  onSelect?: (channelId: string) => void;
130
133
  onFocusRequest?: () => void;
134
+ onCreateConversation?: () => void;
131
135
  onToggleNotifications?: (channelId: string, enabled: boolean) => void;
132
136
  onToggleDirectExpanded?: () => void;
133
137
  }) {
@@ -149,9 +153,9 @@ export function ChannelSidebar({
149
153
  const conversationUnread = conversationChannels.some((channel) => (channelStateById.get(channel.id)?.unreadCount ?? 0) > 0);
150
154
  const sidebarRows = useMemo(() => [
151
155
  ...publicChannels.map((channel) => ({ kind: "channel" as const, channel })),
152
- ...(conversationChannels.length > 0 ? [{ kind: "direct-header" as const }] : []),
156
+ ...(conversationChannels.length > 0 || canCreateConversation ? [{ kind: "direct-header" as const }] : []),
153
157
  ...(directExpanded ? conversationChannels.map((channel) => ({ kind: "channel" as const, channel })) : []),
154
- ], [conversationChannels, directExpanded, publicChannels]);
158
+ ], [canCreateConversation, conversationChannels, directExpanded, publicChannels]);
155
159
  const sidebarLayoutHeight = nativePaneChrome ? "100%" : height;
156
160
  const nativeFillStyle = nativePaneChrome ? { minHeight: 0 } : undefined;
157
161
  const sidebarBorder = borderWidth > 0
@@ -201,6 +205,26 @@ export function ChannelSidebar({
201
205
  >
202
206
  {` ${directExpanded ? "▾" : "▸"} DMs`}
203
207
  </Text>
208
+ <Box flexGrow={1} />
209
+ {canCreateConversation ? (
210
+ <Box
211
+ width={3}
212
+ height={1}
213
+ alignItems="center"
214
+ justifyContent="center"
215
+ backgroundColor={hoveredChannelId === "direct-header:create" ? hoverBg() : sidebarBg}
216
+ onMouseOver={() => setHoveredChannelId("direct-header:create")}
217
+ onMouseOut={() => setHoveredChannelId((current) => (current === "direct-header:create" ? null : current))}
218
+ onMouseDown={(event: any) => {
219
+ event?.preventDefault?.();
220
+ event?.stopPropagation?.();
221
+ onCreateConversation?.();
222
+ }}
223
+ style={{ cursor: "pointer" }}
224
+ >
225
+ <Text fg={hoveredChannelId === "direct-header:create" ? colors.textMuted : colors.textDim}>+</Text>
226
+ </Box>
227
+ ) : null}
204
228
  </Box>
205
229
  );
206
230
  }
@@ -9,7 +9,7 @@ import { Box } from "../../../ui";
9
9
  import { apiClient, type ChatChannel, type ChatMessage } from "../../../api-client";
10
10
  import { PluginRenderProvider, type PluginRuntimeAccess } from "../../runtime";
11
11
  import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../../registry";
12
- import { ChatContent } from "../chat";
12
+ import { ChatContent } from "./content";
13
13
  import { ChatController } from "./controller";
14
14
 
15
15
  const TRANSCRIPT_KIND = "channel-transcript";
@@ -178,23 +178,33 @@ export function createChatTestControls(getSetup: () => ChatTestSetup) {
178
178
  sequence?: string;
179
179
  ctrl?: boolean;
180
180
  meta?: boolean;
181
+ super?: boolean;
182
+ alt?: boolean;
181
183
  shift?: boolean;
182
184
  option?: boolean;
183
185
  }) {
186
+ const keyEvent = {
187
+ ctrl: false,
188
+ meta: false,
189
+ option: false,
190
+ shift: false,
191
+ eventType: "press",
192
+ repeated: false,
193
+ defaultPrevented: false,
194
+ propagationStopped: false,
195
+ preventDefault() {
196
+ keyEvent.defaultPrevented = true;
197
+ },
198
+ stopPropagation() {
199
+ keyEvent.propagationStopped = true;
200
+ },
201
+ ...event,
202
+ };
184
203
  await act(async () => {
185
- getSetup().renderer.keyInput.emit("keypress", {
186
- ctrl: false,
187
- meta: false,
188
- option: false,
189
- shift: false,
190
- eventType: "press",
191
- repeated: false,
192
- stopPropagation: () => {},
193
- preventDefault: () => {},
194
- ...event,
195
- } as any);
204
+ (getSetup().renderer.keyInput as any).emit("keypress", keyEvent);
196
205
  await getSetup().renderOnce();
197
206
  });
207
+ return keyEvent;
198
208
  },
199
209
  };
200
210
  }
@@ -10,7 +10,7 @@ import {
10
10
  CONGRESS_TRADES_PANE_ID,
11
11
  CongressTradesPane,
12
12
  } from "../congress-trades/pane";
13
- import { registerTwitterFeedFeature } from "../cloud-tweets";
13
+ import { registerTwitterFeedFeature } from "../cloud-tweets/registration";
14
14
  import {
15
15
  buildDmCommandResults,
16
16
  formatChatPaneTitle,
@@ -103,6 +103,13 @@ export function createGloomberbCloudPlugin({
103
103
  chatController.attachPersistence(ctx.persistence, ctx.resume);
104
104
  chatController.setNotifier(ctx.notify);
105
105
 
106
+ ctx.registerSyncTransport({
107
+ id: "gloomberb-cloud",
108
+ isAvailable: () => apiClient.isVerified(),
109
+ pullSnapshot: () => apiClient.getSyncSnapshot(),
110
+ pushSnapshot: (snapshot, options) => apiClient.putSyncSnapshot(snapshot, options),
111
+ });
112
+
106
113
  ctx.registerPane({
107
114
  id: "chat",
108
115
  name: "Chat",
@@ -16,7 +16,6 @@ import {
16
16
  TWEET_SEARCH_SCHEMA_VERSION,
17
17
  TWITTER_FEED_LAUNCH_SCHEMA_VERSION,
18
18
  TWITTER_FEED_LAUNCH_STATE_KEY,
19
- TWITTER_FEED_PANE_ID,
20
19
  createFeed,
21
20
  deriveFeedTitle,
22
21
  normalizeFeedQuery,
@@ -1,5 +1,7 @@
1
1
  import type { DataTableColumn } from "../../../components";
2
2
  import { formatCompact } from "../../../utils/format";
3
+
4
+ export { truncateWithEllipsis as truncate } from "../../../utils/text-wrap";
3
5
  import type {
4
6
  CloudCongressMemberPayload,
5
7
  CloudCongressTradePayload,
@@ -76,13 +78,6 @@ export function formatAmountRange(low: number | null, high: number | null, raw?:
76
78
  return formatMoneyShort(low ?? high);
77
79
  }
78
80
 
79
- export function truncate(value: string, width: number): string {
80
- if (width <= 0) return "";
81
- if (value.length <= width) return value;
82
- if (width <= 3) return value.slice(0, width);
83
- return `${value.slice(0, width - 3)}...`;
84
- }
85
-
86
81
  function compareText(left: string, right: string): number {
87
82
  return left.localeCompare(right, "en-US", { sensitivity: "base" });
88
83
  }
@@ -19,7 +19,6 @@ import {
19
19
  CONGRESS_FILING_LIMIT,
20
20
  CONGRESS_MEMBER_FILING_LIMIT,
21
21
  CONGRESS_TRADE_LIMIT,
22
- CONGRESS_TRADES_PANE_ID,
23
22
  buildMemberColumns,
24
23
  buildTradeColumns,
25
24
  nextSort,
@@ -18,7 +18,9 @@ import {
18
18
  } from "./settings";
19
19
  import { resolveCorrelationHeatmapCellColors } from "./colors";
20
20
  import {
21
+ buildRelationshipGraphSettingsDef,
21
22
  createRelationshipPaneTemplate,
23
+ RELATIONSHIP_GRAPH_PANE_ID,
22
24
  RelationshipGraphPane,
23
25
  } from "./relationship/pane";
24
26
  import {
@@ -234,13 +236,14 @@ export const correlationPlugin: GloomPlugin = {
234
236
  settings: buildCorrelationSettingsDef(),
235
237
  },
236
238
  {
237
- id: "relationship-graph",
239
+ id: RELATIONSHIP_GRAPH_PANE_ID,
238
240
  name: "Relationship Graph",
239
241
  icon: "R",
240
242
  component: RelationshipGraphPane,
241
243
  defaultPosition: "right",
242
244
  defaultMode: "floating",
243
245
  defaultFloatingSize: { width: 100, height: 30 },
246
+ settings: buildRelationshipGraphSettingsDef(),
244
247
  },
245
248
  ],
246
249
 
@@ -1,5 +1,6 @@
1
1
  import { Box, Text } from "../../../../ui";
2
2
  import { colors } from "../../../../theme/colors";
3
+ import { Checkbox } from "../../../../components/ui/checkbox";
3
4
 
4
5
  export function RelationshipMetricsTable({
5
6
  rows,
@@ -39,7 +40,7 @@ export function RelationshipToggle({
39
40
  }) {
40
41
  return (
41
42
  <Box
42
- width={label.length + 5}
43
+ width={label.length + 6}
43
44
  height={1}
44
45
  onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
45
46
  event.preventDefault?.();
@@ -47,7 +48,12 @@ export function RelationshipToggle({
47
48
  onPress();
48
49
  }}
49
50
  >
50
- <Text fg={checked ? colors.text : colors.textDim}>{checked ? "[x]" : "[ ]"} {label}</Text>
51
+ <Checkbox
52
+ label={label}
53
+ checked={checked}
54
+ width={label.length + 6}
55
+ onChange={onPress}
56
+ />
51
57
  </Box>
52
58
  );
53
59
  }
@@ -1,12 +1,13 @@
1
1
  import type { ProjectedChartPoint } from "../../../../components/chart/core/data";
2
2
  import type { TimeRange } from "../../../../components/chart/core/types";
3
3
  import type { ScatterChartPoint } from "../../../../components/chart/scatter-chart-renderer";
4
- import type { PaneTemplateCreateOptions } from "../../../../types/plugin";
4
+ import type { PaneSettingsDef, PaneTemplateCreateOptions } from "../../../../types/plugin";
5
5
  import type { PricePoint } from "../../../../types/financials";
6
- import { parseTickerListInput } from "../../../../tickers/list";
6
+ import { formatTickerListInput, parseTickerListInput } from "../../../../tickers/list";
7
7
 
8
8
  export type RelationshipRange = Extract<TimeRange, "1M" | "3M" | "6M" | "1Y" | "5Y" | "ALL">;
9
9
 
10
+ export const RELATIONSHIP_GRAPH_PANE_ID = "relationship-graph";
10
11
  const RELATIONSHIP_RANGES: RelationshipRange[] = ["1M", "3M", "6M", "1Y", "5Y", "ALL"];
11
12
  export const DEFAULT_RELATIONSHIP_SECOND_SYMBOL = "SPY";
12
13
  const RELATIONSHIP_CORRELATION_WINDOWS = [30, 60, 120, 252] as const;
@@ -277,3 +278,22 @@ export function relationshipTemplateSymbols(
277
278
  return null;
278
279
  }
279
280
  }
281
+
282
+ export function buildRelationshipGraphPaneTitle(pair: readonly [string, string]): string {
283
+ return `GR ${pair[0]}/${pair[1]}`;
284
+ }
285
+
286
+ export function buildRelationshipGraphSettingsDef(): PaneSettingsDef {
287
+ return {
288
+ title: "Relationship Graph Settings",
289
+ fields: [
290
+ {
291
+ key: "symbolsText",
292
+ label: "Tickers",
293
+ description: `Enter one or two tickers. One ticker compares against ${DEFAULT_RELATIONSHIP_SECOND_SYMBOL}.`,
294
+ type: "text",
295
+ placeholder: formatTickerListInput(["AMD", "NVDA"]),
296
+ },
297
+ ],
298
+ };
299
+ }
@@ -17,7 +17,9 @@ import { useRelationshipHistories } from "./history";
17
17
  import {
18
18
  DEFAULT_RELATIONSHIP_CORRELATION_WINDOW,
19
19
  DEFAULT_RELATIONSHIP_SECOND_SYMBOL,
20
+ RELATIONSHIP_GRAPH_PANE_ID,
20
21
  buildRelationshipAnalysis,
22
+ buildRelationshipGraphPaneTitle,
21
23
  nextRelationshipRange,
22
24
  nextRelationshipWindow,
23
25
  relationshipSymbolsFromPaneSettings,
@@ -35,7 +37,11 @@ import {
35
37
  formatNullableNumber,
36
38
  } from "./view-model";
37
39
 
38
- export { buildRelationshipAnalysis } from "./model";
40
+ export {
41
+ buildRelationshipAnalysis,
42
+ buildRelationshipGraphSettingsDef,
43
+ RELATIONSHIP_GRAPH_PANE_ID,
44
+ } from "./model";
39
45
 
40
46
  type RelationshipGraphShortcut = "range" | "window" | "correlation" | "regression";
41
47
 
@@ -326,7 +332,7 @@ export function RelationshipGraphPane({ focused, width, height }: PaneProps) {
326
332
  export function createRelationshipPaneTemplate(): PaneTemplateDef {
327
333
  return {
328
334
  id: "relationship-graph-pane",
329
- paneId: "relationship-graph",
335
+ paneId: RELATIONSHIP_GRAPH_PANE_ID,
330
336
  label: "Relationship Graph",
331
337
  description: "Graph ratio, rolling correlation, and regression between two tickers.",
332
338
  keywords: ["relationship", "ratio", "graph", "correlation", "regression", "gr"],
@@ -345,7 +351,7 @@ export function createRelationshipPaneTemplate(): PaneTemplateDef {
345
351
  const pair = relationshipTemplateSymbols(context.activeTicker, options);
346
352
  return pair
347
353
  ? {
348
- title: `GR ${pair[0]}/${pair[1]}`,
354
+ title: buildRelationshipGraphPaneTitle(pair),
349
355
  binding: { kind: "fixed" as const, symbol: pair[0] },
350
356
  placement: "floating" as const,
351
357
  settings: {
@@ -103,15 +103,6 @@ function EarningsCalendarPane({ focused, width, height }: PaneProps) {
103
103
  return false;
104
104
  }, [reload]);
105
105
 
106
- const selectDisplayRow = useCallback((row: EarningsDisplayRow) => {
107
- if (row.kind !== "event") return;
108
- if (row.eventIdx === activeEventIdx) {
109
- openEvent(row.event);
110
- return;
111
- }
112
- setSelectedIdx(row.eventIdx);
113
- }, [activeEventIdx, openEvent, setSelectedIdx]);
114
-
115
106
  const renderCell = useCallback((
116
107
  row: EarningsDisplayRow,
117
108
  column: EarningsColumn,
@@ -1,4 +1,4 @@
1
- import { Box, Text } from "../../../ui";
1
+ import { Box } from "../../../ui";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { TextAttributes, type ScrollBoxRenderable } from "../../../ui";
4
4
  import { DataTableStackView, usePaneFooter, type DataTableCell } from "../../../components";
@@ -243,10 +243,6 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
243
243
  }), [countryFilter, cycleCountryFilter, cycleImpactFilter, error, impactFilter, load, loading, nextCountdown, nextEvent?.event, staleness]);
244
244
 
245
245
  const handleHeaderClick = useCallback(() => {}, []);
246
- const selectDisplayRow = useCallback((row: DisplayRow) => {
247
- if (row.kind !== "event") return;
248
- setSelectedIdx(row.eventIdx);
249
- }, []);
250
246
  const openDisplayRow = useCallback((row: DisplayRow) => {
251
247
  if (row.kind !== "event") return;
252
248
  setDetailEvent(row.event);