gloomberb 0.9.8 → 0.9.10

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 (181) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/index.test.ts +3 -3
  3. package/src/api-client/index.ts +0 -6
  4. package/src/api-client/request.test.ts +63 -0
  5. package/src/api-client/request.ts +53 -5
  6. package/src/api-client/types.ts +0 -2
  7. package/src/app/pane-runtime/layout-placement.ts +0 -1
  8. package/src/app.tsx +1 -0
  9. package/src/brokers/account-cache.ts +2 -10
  10. package/src/brokers/broker-portfolio-sync.ts +0 -1
  11. package/src/brokers/broker-ticker-sync.ts +20 -16
  12. package/src/brokers/remote-broker-adapter.ts +3 -0
  13. package/src/capabilities/registry.test.ts +25 -0
  14. package/src/capabilities/registry.ts +22 -5
  15. package/src/cli/commands/automation.ts +1 -3
  16. package/src/cli/commands/command-utils.ts +0 -7
  17. package/src/cli/commands/overview.ts +2 -2
  18. package/src/cli/commands/plugins.ts +34 -17
  19. package/src/cli/index.ts +2 -2
  20. package/src/cli.test.ts +0 -157
  21. package/src/components/chart/comparison/renderer.ts +0 -1
  22. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  23. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  24. package/src/components/chart/core/index.test.ts +0 -24
  25. package/src/components/chart/core/renderer.ts +0 -4
  26. package/src/components/chart/core/resolution.test.ts +0 -17
  27. package/src/components/chart/core/resolution.ts +0 -26
  28. package/src/components/chart/core/scroll.test.ts +0 -6
  29. package/src/components/chart/core/scroll.ts +0 -22
  30. package/src/components/chart/core/viewport.test.ts +2 -23
  31. package/src/components/chart/core/viewport.ts +0 -27
  32. package/src/components/chart/market-session.ts +1 -1
  33. package/src/components/chart/native/surface/hooks.ts +10 -3
  34. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  35. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  36. package/src/components/command-bar/helpers.test.ts +1 -80
  37. package/src/components/command-bar/helpers.ts +1 -36
  38. package/src/components/command-bar/pane-settings.ts +1 -1
  39. package/src/components/command-bar/panel/runtime.ts +0 -2
  40. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  41. package/src/components/command-bar/surface/index.test.tsx +0 -16
  42. package/src/components/command-bar/surface/index.tsx +0 -1
  43. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  44. package/src/components/feed-data-table/stack-view.tsx +0 -1
  45. package/src/components/layout/shell/index.test.tsx +2 -113
  46. package/src/components/layout/shell/index.tsx +0 -1
  47. package/src/components/layout/shell/menu.ts +0 -1
  48. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  49. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  50. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  51. package/src/components/price-selector-dialog.tsx +3 -3
  52. package/src/components/ticker/badge/text.tsx +0 -1
  53. package/src/components/ui/page-stack-view.tsx +1 -1
  54. package/src/components/ui/ui.test.tsx +0 -78
  55. package/src/core/state/app/layout-reducer.ts +0 -1
  56. package/src/data/config/layout.ts +3 -3
  57. package/src/data/resource-store.ts +6 -0
  58. package/src/data/sqlite/cache.test.ts +5 -0
  59. package/src/data/ticker-store.ts +0 -7
  60. package/src/market-data/coordinator/auxiliary.ts +1 -44
  61. package/src/market-data/coordinator/entries.ts +0 -1
  62. package/src/market-data/coordinator/index.test.ts +0 -10
  63. package/src/market-data/coordinator/index.ts +1 -18
  64. package/src/market-data/coordinator/quotes.ts +0 -6
  65. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  66. package/src/market-data/market/status.ts +19 -1
  67. package/src/market-data/query-store.ts +0 -4
  68. package/src/market-data/quotes/resolution.ts +2 -2
  69. package/src/market-data/request-types.ts +0 -5
  70. package/src/market-data/result-types.ts +0 -11
  71. package/src/market-data/selectors.ts +1 -5
  72. package/src/news/aggregator.ts +5 -1
  73. package/src/pane-settings.test.ts +0 -18
  74. package/src/plugins/builtin/account-management/form-components.tsx +0 -120
  75. package/src/plugins/builtin/account-management/model.ts +3 -27
  76. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  77. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  78. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  79. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  80. package/src/plugins/builtin/buildout/format.ts +2 -11
  81. package/src/plugins/builtin/chat/chat.test.tsx +2 -1
  82. package/src/plugins/builtin/chat/content/index.tsx +1 -2
  83. package/src/plugins/builtin/chat/content/transcript.tsx +0 -1
  84. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  85. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  86. package/src/plugins/builtin/chat/sidebar.test.tsx +15 -5
  87. package/src/plugins/builtin/chat/test-harness.tsx +2 -2
  88. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  89. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  90. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  91. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  92. package/src/plugins/builtin/earnings/index.tsx +0 -9
  93. package/src/plugins/builtin/econ/index.tsx +1 -5
  94. package/src/plugins/builtin/help/index.tsx +2 -2
  95. package/src/plugins/builtin/holders/pane.tsx +0 -5
  96. package/src/plugins/builtin/insider/index.tsx +1 -8
  97. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  98. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  99. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  100. package/src/plugins/builtin/news/index.tsx +0 -1
  101. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  102. package/src/plugins/builtin/options/view.test.tsx +1 -38
  103. package/src/plugins/builtin/plugin-groups.ts +17 -3
  104. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  105. package/src/plugins/builtin/portfolio-list/index.test.ts +1 -24
  106. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  107. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  108. package/src/plugins/builtin/sec/index.tsx +0 -2
  109. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  110. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  111. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  112. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +1 -1
  113. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  114. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  115. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  116. package/src/plugins/builtin/world-indices/model.ts +0 -7
  117. package/src/plugins/ibkr/account-cache.ts +1 -1
  118. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  119. package/src/plugins/ibkr/trade/utils.ts +1 -7
  120. package/src/plugins/loader.ts +2 -1
  121. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  122. package/src/plugins/registry/contributions.ts +22 -11
  123. package/src/plugins/registry/index.test.ts +44 -0
  124. package/src/plugins/registry/index.ts +44 -26
  125. package/src/remote/controller.ts +1 -7
  126. package/src/remote/json-patch.ts +15 -1
  127. package/src/remote/server.ts +16 -6
  128. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  129. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  130. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  131. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  132. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  133. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  134. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  135. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  136. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  137. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  138. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  139. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  140. package/src/renderers/opentui/host.tsx +11 -3
  141. package/src/renderers/opentui/input-host.tsx +5 -1
  142. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  143. package/src/sources/gloomberb-cloud/index.ts +1 -30
  144. package/src/sources/gloomberb-cloud/news.ts +0 -12
  145. package/src/sources/provider-router/financial-routes.ts +25 -3
  146. package/src/sources/provider-router/index.test.ts +167 -0
  147. package/src/sources/provider-router/search.ts +5 -0
  148. package/src/sources/provider-router/supplemental.ts +4 -3
  149. package/src/sources/yahoo-finance/history.ts +5 -1
  150. package/src/sources/yahoo-finance/options.ts +59 -14
  151. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  152. package/src/sources/yahoo-finance.test.ts +40 -21
  153. package/src/sources/yahoo-finance.ts +13 -8
  154. package/src/state/app/context/index.test.tsx +0 -81
  155. package/src/state/app/context/selector.test.tsx +6 -2
  156. package/src/sync/controller.test.ts +151 -0
  157. package/src/sync/controller.ts +92 -57
  158. package/src/sync/core-contributors.ts +45 -21
  159. package/src/sync/react.ts +6 -8
  160. package/src/sync/types.ts +3 -0
  161. package/src/test-support/data-provider.ts +0 -1
  162. package/src/ui/host.tsx +2 -1
  163. package/src/ui/index.tsx +1 -0
  164. package/src/updater.test.ts +0 -21
  165. package/src/updater.ts +20 -16
  166. package/src/utils/async-deadline.ts +26 -0
  167. package/src/utils/selection-clipboard.ts +2 -2
  168. package/src/version.ts +1 -1
  169. package/src/components/layout/pane/header.test.tsx +0 -39
  170. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  171. package/src/plugins/builtin/chat/index.tsx +0 -4
  172. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  173. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  174. package/src/plugins/builtin/help/index.test.tsx +0 -242
  175. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  176. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  177. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  178. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  179. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  180. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  181. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -84,54 +84,6 @@ export function FieldRow({
84
84
  );
85
85
  }
86
86
 
87
- export function PickerRow({
88
- label,
89
- value,
90
- detail,
91
- active,
92
- width,
93
- onFocus,
94
- onOpen,
95
- }: {
96
- label: string;
97
- value: string;
98
- detail?: string;
99
- active: boolean;
100
- width: number;
101
- onFocus: () => void;
102
- onOpen: () => void;
103
- }) {
104
- const buttonWidth = Math.max(12, Math.min(32, width - 18));
105
- const detailWidth = Math.max(0, width - buttonWidth - 4);
106
-
107
- return (
108
- <Box
109
- flexDirection="column"
110
- onMouseOver={onFocus}
111
- onMouseDown={(event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
112
- event?.stopPropagation?.();
113
- event?.preventDefault?.();
114
- onFocus();
115
- onOpen();
116
- }}
117
- >
118
- <Text fg={active ? colors.textBright : colors.textDim} attributes={active ? TextAttributes.BOLD : 0}>
119
- {active ? `> ${label}` : ` ${label}`}
120
- </Text>
121
- <Box height={1} flexDirection="row" gap={1}>
122
- <Box width={buttonWidth} backgroundColor={active ? colors.selected : colors.panel}>
123
- <Text fg={active ? colors.selectedText : colors.text}>
124
- {` ${truncate(value, Math.max(1, buttonWidth - 2))} `}
125
- </Text>
126
- </Box>
127
- {detail ? (
128
- <Text fg={colors.textMuted}>{truncate(detail, detailWidth)}</Text>
129
- ) : null}
130
- </Box>
131
- </Box>
132
- );
133
- }
134
-
135
87
  export function CheckboxRow({
136
88
  label,
137
89
  checked,
@@ -207,78 +159,6 @@ function metricColor(tone: ProfileAnalyticsPreview["metrics"][number]["tone"]):
207
159
  return colors.text;
208
160
  }
209
161
 
210
- export function AccountAnalyticsPreview({
211
- preview,
212
- width,
213
- disclaimer,
214
- surface = "panel",
215
- }: {
216
- preview: ProfileAnalyticsPreview;
217
- width: number;
218
- disclaimer?: string | null;
219
- surface?: "panel" | "plain";
220
- }) {
221
- const horizontalPadding = surface === "panel" ? 1 : 0;
222
- const contentWidth = Math.max(1, width - horizontalPadding * 2);
223
- const metricWidth = Math.max(14, Math.min(28, Math.floor((contentWidth - 2) / 2)));
224
- const subtitleWidth = Math.max(1, contentWidth - preview.title.length - 2);
225
- const showHeader = surface === "panel";
226
-
227
- return (
228
- <Box
229
- flexDirection="column"
230
- width={width}
231
- backgroundColor={surface === "panel" ? colors.panel : undefined}
232
- paddingX={horizontalPadding}
233
- >
234
- {showHeader ? (
235
- <Box height={1} flexDirection="row">
236
- <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>
237
- {truncate(preview.title, contentWidth)}
238
- </Text>
239
- {preview.subtitle ? (
240
- <>
241
- <Box flexGrow={1} />
242
- <Text fg={colors.textMuted}>{truncate(preview.subtitle, subtitleWidth)}</Text>
243
- </>
244
- ) : null}
245
- </Box>
246
- ) : null}
247
- {preview.metrics.length > 0 ? (
248
- <Box flexDirection="row" flexWrap="wrap" gap={2}>
249
- {preview.metrics.map((metric) => {
250
- const labelWidth = Math.max(1, Math.min(metric.label.length, metricWidth - 2));
251
- const valueWidth = Math.max(1, metricWidth - labelWidth - 1);
252
- const detailWidth = Math.max(0, metricWidth - labelWidth - metric.value.length - 2);
253
- return (
254
- <Box key={metric.id} width={metricWidth} height={1} flexDirection="row" gap={1}>
255
- <Text fg={colors.textDim}>{truncate(metric.label, labelWidth)}</Text>
256
- <Text fg={metricColor(metric.tone)} attributes={TextAttributes.BOLD}>
257
- {truncate(metric.value, valueWidth)}
258
- </Text>
259
- {metric.detail ? (
260
- <Text fg={colors.textMuted}>
261
- {truncate(metric.detail, detailWidth)}
262
- </Text>
263
- ) : null}
264
- </Box>
265
- );
266
- })}
267
- </Box>
268
- ) : preview.subtitle ? (
269
- <Text fg={colors.textMuted} wrapText width={contentWidth}>
270
- {preview.subtitle}
271
- </Text>
272
- ) : null}
273
- {disclaimer ? (
274
- <Text fg={colors.textMuted} wrapText width={contentWidth}>
275
- {disclaimer}
276
- </Text>
277
- ) : null}
278
- </Box>
279
- );
280
- }
281
-
282
162
  export function PublicAnalyticsGroup({
283
163
  preview,
284
164
  choices,
@@ -3,6 +3,8 @@ import type { AccountProfile, PublicPortfolioAnalytics } from "../../../api-clie
3
3
  import type { Portfolio, TickerRecord } from "../../../types/ticker";
4
4
  import { formatNumber } from "../../../utils/format";
5
5
 
6
+ export { truncateWithEllipsis as truncate } from "../../../utils/text-wrap";
7
+
6
8
  export type AccountFieldKey =
7
9
  | "username"
8
10
  | "name"
@@ -36,7 +38,7 @@ export interface AccountDraft {
36
38
  positionAlertsEnabled: boolean;
37
39
  }
38
40
 
39
- export interface ProfileAnalyticsPreviewMetric {
41
+ interface ProfileAnalyticsPreviewMetric {
40
42
  id: string;
41
43
  label: string;
42
44
  value: string;
@@ -52,25 +54,6 @@ export interface ProfileAnalyticsPreview {
52
54
  publicAnalytics: PublicPortfolioAnalytics | null;
53
55
  }
54
56
 
55
- export const BASE_FIELD_ORDER: AccountFieldKey[] = [
56
- "username",
57
- "name",
58
- "company",
59
- "title",
60
- "bio",
61
- "profilePublic",
62
- "publicEmail",
63
- "xAccount",
64
- "acceptUnknownDms",
65
- "sharedPortfolioId",
66
- "weeklyRoundupEnabled",
67
- "positionAlertsEnabled",
68
- "emailAlertsOffAction",
69
- "upgradeAction",
70
- "passwordAction",
71
- "deleteAccountAction",
72
- ];
73
-
74
57
  export const NO_PORTFOLIO_VALUE = "__none__";
75
58
 
76
59
  export function profileToDraft(profile: AccountProfile | null): AccountDraft {
@@ -95,13 +78,6 @@ export function emptyToNull(value: string): string | null {
95
78
  return trimmed ? trimmed : null;
96
79
  }
97
80
 
98
- export function truncate(value: string, width: number): string {
99
- if (width <= 0) return "";
100
- if (value.length <= width) return value;
101
- if (width <= 3) return value.slice(0, width);
102
- return `${value.slice(0, width - 3)}...`;
103
- }
104
-
105
81
  export function formatPlan(plan: AccountProfile["plan"] | null | undefined): string {
106
82
  return plan === "pro" ? "Pro" : "Free";
107
83
  }
@@ -112,38 +112,6 @@ async function pressKey(key: string) {
112
112
  }
113
113
 
114
114
  describe("BrokersPane", () => {
115
- test("shows empty state and opens add broker flow", async () => {
116
- const calls: string[] = [];
117
- testSetup = await testRender(<Harness calls={calls} />, { width: 92, height: 24 });
118
- await act(async () => {
119
- await testSetup!.renderOnce();
120
- });
121
-
122
- const frame = testSetup.captureCharFrame();
123
- expect(frame).toContain("No broker profiles.");
124
- expect(frame).not.toContain("Press a or click Add Broker.");
125
- await act(async () => {
126
- testSetup!.mockInput.pressKey("a");
127
- await testSetup!.renderOnce();
128
- });
129
-
130
- expect(calls).toEqual(["command:Add Broker Account"]);
131
- });
132
-
133
- test("hides unavailable footer actions with no broker profile", async () => {
134
- const calls: string[] = [];
135
- testSetup = await testRender(<FooterHarness calls={calls} />, { width: 92, height: 25 });
136
- await act(async () => {
137
- await testSetup!.renderOnce();
138
- await testSetup!.renderOnce();
139
- });
140
-
141
- const frame = testSetup.captureCharFrame();
142
- expect(frame).toContain("[a]dd");
143
- expect(frame).not.toContain("[c]onnect");
144
- expect(frame).not.toContain("[d]isconnect");
145
- });
146
-
147
115
  test("renders IBKR row and invokes broker actions", async () => {
148
116
  const calls: string[] = [];
149
117
  testSetup = await testRender(<FooterHarness calls={calls} instance={createGatewayInstance()} height={35} />, { width: 92, height: 35 });
@@ -1,6 +1,7 @@
1
1
  import { TextAttributes } from "../../../ui";
2
2
  import type { DataTableCell } from "../../../components";
3
3
  import { colors } from "../../../theme/colors";
4
+ import { formatRelativeTime } from "../../../utils/datetime-format";
4
5
  import {
5
6
  CompanyCell,
6
7
  FavoriteCell,
@@ -11,7 +12,6 @@ import {
11
12
  activityColor,
12
13
  activityLabel,
13
14
  criticalityColor,
14
- formatRelativeTime,
15
15
  metricColor,
16
16
  text,
17
17
  tickerSymbol,
@@ -2,7 +2,7 @@ import { Box, Text } from "../../../../ui";
2
2
  import { RemoteImage } from "../../../../components/ui";
3
3
  import { colors } from "../../../../theme/colors";
4
4
  import type { BuildoutObservation, BuildoutSite } from "../model/types";
5
- import { dateShort, truncate } from "../format";
5
+ import { dateShort } from "../format";
6
6
 
7
7
  function observationImageUrl(observation: BuildoutObservation) {
8
8
  return observation.upscaledImageUrl
@@ -6,7 +6,6 @@ import { colors } from "../../../../theme/colors";
6
6
  import type { InlineTickerCatalogEntry } from "../../../../state/hooks/inline-tickers";
7
7
  import type { BuildoutCompany, BuildoutRelatedCompany, BuildoutSource } from "../model/types";
8
8
  import { domainFromUrl, text, textOrNull, tickerSymbol, truncate } from "../format";
9
- import { booleanText } from "./values";
10
9
 
11
10
  export type InlineTickerCatalog = Record<string, InlineTickerCatalogEntry>;
12
11
 
@@ -1,13 +1,8 @@
1
1
  import { colors, priceColor } from "../../../theme/colors";
2
- import { formatDetailDate as sharedFormatDetailDate, formatRelativeTime as sharedFormatRelativeTime, parseDisplayDate } from "../../../utils/datetime-format";
2
+ import { formatDetailDate as sharedFormatDetailDate, parseDisplayDate } from "../../../utils/datetime-format";
3
3
  import type { BuildoutSource, BuildoutUpdate } from "./model/types";
4
4
 
5
- export function truncate(text: string, width: number) {
6
- if (width <= 0) return "";
7
- if (text.length <= width) return text;
8
- if (width <= 3) return text.slice(0, width);
9
- return `${text.slice(0, width - 3)}...`;
10
- }
5
+ export { truncateWithEllipsis as truncate } from "../../../utils/text-wrap";
11
6
 
12
7
  export function text(value: unknown, fallback = "-") {
13
8
  if (value == null) return fallback;
@@ -62,10 +57,6 @@ export function dateDetail(value: string | null | undefined) {
62
57
  return formatted || null;
63
58
  }
64
59
 
65
- export function formatRelativeTime(value: string | null | undefined): string {
66
- return sharedFormatRelativeTime(value);
67
- }
68
-
69
60
  export function activityLabel(value: number | null | undefined) {
70
61
  if (value == null) return "-";
71
62
  if (value >= 2) return "High";
@@ -9,7 +9,8 @@ import type { ChatMessage } from "../../../api-client";
9
9
  import { apiClient } from "../../../api-client";
10
10
  import { PluginRenderProvider } from "../../runtime";
11
11
  import { setSharedRegistryForTests } from "../../registry";
12
- import { ChatContent, ChatStatusWidget } from ".";
12
+ import { ChatContent } from "./content";
13
+ import { ChatStatusWidget } from "./status-widget";
13
14
  import {
14
15
  cleanupChatTest,
15
16
  createChatTestControls,
@@ -311,11 +311,10 @@ export function ChatContent({
311
311
  ? await controller.openDirectChannel({ username: usernames[0] })
312
312
  : await controller.openGroupChannel({ usernames });
313
313
  setDirectExpanded(true);
314
- channelIdRef.current = channel.id;
315
314
  selectSidebarChannel(channel.id);
316
315
  setSidebarFocused(false);
317
316
  closeNewDmDialog();
318
- }, [channelIdRef, closeNewDmDialog, controller, selectSidebarChannel, setDirectExpanded, setSidebarFocused]);
317
+ }, [closeNewDmDialog, controller, selectSidebarChannel, setDirectExpanded, setSidebarFocused]);
319
318
 
320
319
  useEffect(() => {
321
320
  if (!focused && newDmOpen) {
@@ -69,7 +69,6 @@ export function ChatTranscript({
69
69
  setHoveredIdx,
70
70
  showProfilePopover,
71
71
  stickyTranscript,
72
- user,
73
72
  userByUsername,
74
73
  }: ChatTranscriptProps) {
75
74
  return (
@@ -30,7 +30,6 @@ import {
30
30
  disposeChatControllerRuntime,
31
31
  resetChatControllerRuntime,
32
32
  } from "./lifecycle";
33
- import type { ChatSessionUser } from "./persistence";
34
33
  import {
35
34
  closeAllChannelConnections,
36
35
  closeInactiveChannelConnections,
@@ -69,15 +68,6 @@ export class ChatController {
69
68
  private notifyFn: (notification: AppNotificationRequest) => void = () => {};
70
69
  private notifiedMessageIds = new Set<string>();
71
70
 
72
- private get hydrated(): boolean { return this.session.hydrated; }
73
- private set hydrated(value: boolean) { this.session.hydrated = value; }
74
- private get sessionToken(): string | null { return this.session.sessionToken; }
75
- private set sessionToken(value: string | null) { this.session.sessionToken = value; }
76
- private get sessionChecked(): boolean { return this.session.sessionChecked; }
77
- private set sessionChecked(value: boolean) { this.session.sessionChecked = value; }
78
- private get user(): ChatSessionUser | null { return this.session.user; }
79
- private set user(value: ChatSessionUser | null) { this.session.user = value; }
80
-
81
71
  private readonly storage = new ChatControllerStorage({
82
72
  emit: (channelId) => this.emit(channelId),
83
73
  getSessionToken: () => this.session.sessionToken,
@@ -245,8 +245,8 @@ describe("ChatController", () => {
245
245
  const controller = new ChatController();
246
246
 
247
247
  apiClient.setSessionToken("token-123");
248
- (controller as any).sessionToken = "token-123";
249
- (controller as any).user = { id: "u1", username: "vince", emailVerified: false };
248
+ (controller as any).session.sessionToken = "token-123";
249
+ (controller as any).session.user = { id: "u1", username: "vince", emailVerified: false };
250
250
 
251
251
  controller.setAppActive(false);
252
252
  (controller as any).realtime.syncVerificationPolling();
@@ -263,12 +263,12 @@ describe("ChatController", () => {
263
263
  let closed = false;
264
264
 
265
265
  apiClient.setSessionToken("token-123");
266
- (controller as any).sessionToken = "token-123";
267
- (controller as any).user = { id: "u1", username: "vince", emailVerified: false };
266
+ (controller as any).session.sessionToken = "token-123";
267
+ (controller as any).session.user = { id: "u1", username: "vince", emailVerified: false };
268
268
  (controller as any).realtime.syncVerificationPolling();
269
269
  expect((controller as any).realtime.verificationPollTimer).not.toBeNull();
270
270
 
271
- (controller as any).user = { id: "u1", username: "vince", emailVerified: true };
271
+ (controller as any).session.user = { id: "u1", username: "vince", emailVerified: true };
272
272
  apiClient.connectChannel = () => ({
273
273
  send: async () => {
274
274
  throw new Error("not implemented");
@@ -8,7 +8,7 @@ import { TextAttributes } from "../../../ui";
8
8
  import { apiClient } from "../../../api-client";
9
9
  import { PluginRenderProvider } from "../../runtime";
10
10
  import { gloomberbCloudPlugin } from "../cloud";
11
- import { ChatContent } from "../chat";
11
+ import { ChatContent } from "./content";
12
12
  import { chatController } from "./controller";
13
13
  import { useChatChannelNavigation } from "./content/channel-navigation";
14
14
  import {
@@ -40,7 +40,11 @@ afterEach(async () => {
40
40
  testSetup = undefined;
41
41
  });
42
42
 
43
- 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
+ ) {
44
48
  const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
45
49
 
46
50
  return function ChannelPane() {
@@ -54,7 +58,10 @@ function createChannelPane(controller: ReturnType<typeof createController>, init
54
58
  height={12}
55
59
  focused
56
60
  channelId={channelId}
57
- onChannelChange={setChannelId}
61
+ onChannelChange={(nextChannelId) => {
62
+ onChannelChange?.(nextChannelId);
63
+ setChannelId(nextChannelId);
64
+ }}
58
65
  />
59
66
  </PluginRenderProvider>
60
67
  </AppContext>
@@ -240,7 +247,10 @@ describe("ChatContent channel sidebar", () => {
240
247
  dmUser: { id: "u2", username: "bob", displayName: "Bob" },
241
248
  };
242
249
  };
243
- const ChannelPane = createChannelPane(controller, "everyone");
250
+ const selectedChannels: string[] = [];
251
+ const ChannelPane = createChannelPane(controller, "everyone", (channelId) => {
252
+ selectedChannels.push(channelId);
253
+ });
244
254
 
245
255
  try {
246
256
  await act(async () => {
@@ -275,6 +285,7 @@ describe("ChatContent channel sidebar", () => {
275
285
  await flushFrame();
276
286
 
277
287
  expect(openedTargets).toEqual([{ username: "bob" }]);
288
+ expect(selectedChannels).toEqual(["dm:bob"]);
278
289
  expect(setup().captureCharFrame()).toContain("@bob");
279
290
  expect(setup().captureCharFrame()).not.toContain("New DM");
280
291
  } finally {
@@ -355,7 +366,6 @@ describe("ChatContent channel sidebar", () => {
355
366
  });
356
367
 
357
368
  test("coalesces rapid sidebar navigation to the final channel", async () => {
358
- const controller = createController({ sessionToken: "token-123" });
359
369
  const channels = [
360
370
  { id: "everyone", name: "everyone", created_at: "2026-03-26T12:10:05.684Z" },
361
371
  { id: "equities", name: "equities", created_at: "2026-05-09T00:00:00.000Z" },
@@ -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";
@@ -201,7 +201,7 @@ export function createChatTestControls(getSetup: () => ChatTestSetup) {
201
201
  ...event,
202
202
  };
203
203
  await act(async () => {
204
- getSetup().renderer.keyInput.emit("keypress", keyEvent as any);
204
+ (getSetup().renderer.keyInput as any).emit("keypress", keyEvent);
205
205
  await getSetup().renderOnce();
206
206
  });
207
207
  return keyEvent;
@@ -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,
@@ -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,
@@ -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);
@@ -31,7 +31,7 @@ const HELP_TABS = [
31
31
  type HelpTabId = typeof HELP_TABS[number]["value"];
32
32
  const GLOOMBERB_ISSUES_URL = "https://github.com/vincelwt/gloomberb/issues";
33
33
 
34
- export function HelpPane({ focused, width, height }: PaneProps) {
34
+ function HelpPane({ focused, width, height }: PaneProps) {
35
35
  const registry = getSharedRegistry();
36
36
  const { openCommandBar, showPane } = usePluginAppActions();
37
37
  const [activeTabId, setActiveTabId] = useState<HelpTabId>("basics");
@@ -381,7 +381,7 @@ export function HelpPane({ focused, width, height }: PaneProps) {
381
381
  <Box flexDirection="column" width={width} height={height}>
382
382
  <Box width={width} height={1} flexShrink={0}>
383
383
  <Tabs
384
- tabs={HELP_TABS}
384
+ tabs={[...HELP_TABS]}
385
385
  activeValue={activeTabId}
386
386
  onSelect={(value) => setActiveTabId(value as HelpTabId)}
387
387
  focused={focused}
@@ -127,11 +127,6 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
127
127
  };
128
128
  }, [sortedRows]);
129
129
 
130
- const selectIndex = useCallback((index: number) => {
131
- const row = sortedRows[index];
132
- if (row) setSelectedId(row.id);
133
- }, [sortedRows]);
134
-
135
130
  const selectedRow = activeIdx >= 0 ? sortedRows[activeIdx] ?? null : null;
136
131
  const selectedFundMatch = selectedRow ? fundMatches.get(selectedRow.id) ?? null : null;
137
132
 
@@ -14,6 +14,7 @@ import { FeedDataTableStackView, Spinner, useExternalLinkFooter, type FeedDataTa
14
14
  import { usePluginPaneState } from "../../runtime";
15
15
  import { isUsEquityTicker } from "../../../utils/sec";
16
16
  import { formatCompact, formatCurrency } from "../../../utils/format";
17
+ import { truncateWithEllipsis as truncateText } from "../../../utils/text-wrap";
17
18
  import { parseForm4Xml, type InsiderTransaction } from "./insider-data";
18
19
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
19
20
  import {
@@ -61,13 +62,6 @@ function buildSummary(parsed: ParsedFiling[]): string {
61
62
  return parts.join(" | ");
62
63
  }
63
64
 
64
- function truncateText(text: string, width: number): string {
65
- if (width <= 0) return "";
66
- if (text.length <= width) return text;
67
- if (width <= 3) return text.slice(0, width);
68
- return `${text.slice(0, width - 3)}...`;
69
- }
70
-
71
65
  function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
72
66
  return parsed.map(({ filing, transaction, isLoading }) => {
73
67
  const filingMeta = [
@@ -95,7 +89,6 @@ function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
95
89
  eyebrow: transaction.reportedName,
96
90
  title: buildInsiderTransactionTitle(transaction),
97
91
  timestamp: transaction.filingDate,
98
- preview: transaction.title || undefined,
99
92
  detailTitle: transaction.reportedName,
100
93
  detailMeta: [
101
94
  ...(transaction.title ? [transaction.title] : []),
@@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { act } from "react";
3
3
  import { testRender } from "../../../renderers/opentui/test-utils";
4
4
  import { setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
5
- import type { TickerRecord } from "../../../types/ticker";
6
5
  import { KellySizerHarness, createFinancials, createSizerConfig, createTicker } from "./test-support";
7
6
 
8
7
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
@@ -36,31 +35,6 @@ afterEach(async () => {
36
35
  });
37
36
 
38
37
  describe("KellySizerPane", () => {
39
- test("renders binary sizing context, result, and curve", async () => {
40
- await renderPane();
41
- await flushFrame();
42
-
43
- const frame = testSetup!.captureCharFrame();
44
- expect(frame).toContain("AAPL · Main Portfolio");
45
- expect(frame).toContain("Bankroll");
46
- expect(frame).toContain("100000");
47
- expect(frame).toContain("Win p");
48
- expect(frame).toContain("Full Kelly");
49
- expect(frame).toContain("Clipped");
50
- expect(frame).toContain("Target val");
51
- expect(frame).toContain("Current %");
52
- expect(frame).toContain("Kelly Curve");
53
- expect(frame).toContain("4.0% -> 8.0%");
54
- expect(frame).toContain("growth");
55
- expect(frame).toContain("loss cap");
56
- expect(frame).toContain("cap max loss, max name");
57
- expect(frame).toContain("full");
58
- expect(frame).toContain("159%");
59
- expect(frame).toContain("636%");
60
- expect(frame).not.toContain("Mode");
61
- expect(frame).not.toContain("Sensitivity");
62
- });
63
-
64
38
  test("prefills shared caps from plugin config", async () => {
65
39
  const config = {
66
40
  ...createSizerConfig(),
@@ -43,5 +43,3 @@ export const kellySizerPlugin: GloomPlugin = {
43
43
  },
44
44
  ],
45
45
  };
46
-
47
- export default kellySizerPlugin;
@@ -51,8 +51,6 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
51
51
  const selectedIdx = selectedSymbol
52
52
  ? rows.findIndex((row) => row.symbol === selectedSymbol)
53
53
  : -1;
54
- const activeIdx = selectedIdx >= 0 ? selectedIdx : (rows.length > 0 ? 0 : -1);
55
-
56
54
  useEffect(() => {
57
55
  if (selectedSymbol && selectedIdx >= 0) return;
58
56
  const firstRow = rows[0];