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
@@ -1,20 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { PaneTemplateDef, WizardStep } from "../../types/plugin";
3
- import type {
4
- CommandBarRoute,
5
- CommandBarWorkflowField,
6
- } from "./workflow/types";
3
+ import type { CommandBarWorkflowField } from "./workflow/types";
7
4
  import {
8
5
  buildGeneratedTemplateField,
9
6
  getCollectionCommandAction,
10
7
  getCollectionCommandKind,
11
8
  getCollectionCommandVerb,
12
9
  getFirstVisibleFieldId,
13
- getScreenFooterLeft,
14
- getScreenFooterRight,
15
10
  getVisibleWorkflowFields,
16
11
  isCollectionCommand,
17
- isRootParsedCommand,
18
12
  isRouteCommandId,
19
13
  isWorkflowTextField,
20
14
  looksDestructiveCommand,
@@ -22,7 +16,6 @@ import {
22
16
  routeCommandIdToScreen,
23
17
  slugifyName,
24
18
  summarizeError,
25
- summarizePaneSettingValue,
26
19
  summarizeWorkflowFieldValue,
27
20
  } from "./helpers";
28
21
 
@@ -79,26 +72,6 @@ describe("command-bar helpers", () => {
79
72
  }, ["symbol", "price", "pnl", "volume"])).toBe("Symbol, Price, P&L +1");
80
73
  });
81
74
 
82
- test("summarizes pane setting values consistently", () => {
83
- expect(summarizePaneSettingValue({
84
- key: "theme",
85
- label: "Theme",
86
- type: "select",
87
- options: [{ label: "Nord", value: "nord" }],
88
- }, "nord")).toBe("Nord");
89
- expect(summarizePaneSettingValue({
90
- key: "columns",
91
- label: "Columns",
92
- type: "ordered-multi-select",
93
- options: [
94
- { label: "Symbol", value: "symbol" },
95
- { label: "Price", value: "price" },
96
- { label: "P&L", value: "pnl" },
97
- { label: "Volume", value: "volume" },
98
- ],
99
- }, ["symbol", "price", "pnl", "volume"])).toBe("Symbol, Price, P&L +1");
100
- });
101
-
102
75
  test("normalizes wizard fields and derives defaults from steps", () => {
103
76
  const steps: WizardStep[] = [
104
77
  { key: "intro", label: "Intro", type: "info", body: ["Line one", "Line two"] },
@@ -157,61 +130,9 @@ describe("command-bar helpers", () => {
157
130
  });
158
131
  });
159
132
 
160
- test("formats screen footers based on route context", () => {
161
- const orderedPickerRoute: CommandBarRoute = {
162
- kind: "picker",
163
- pickerId: "field-multi-select",
164
- title: "Columns",
165
- query: "",
166
- selectedIdx: 0,
167
- hoveredIdx: null,
168
- options: [],
169
- payload: { fieldType: "ordered-multi-select" },
170
- };
171
- const filteredPickerRoute: CommandBarRoute = {
172
- ...orderedPickerRoute,
173
- query: "vol",
174
- };
175
- const workflowRoute: CommandBarRoute = {
176
- kind: "workflow",
177
- workflowId: "create-pane",
178
- title: "Create Pane",
179
- fields: [],
180
- values: {},
181
- activeFieldId: null,
182
- submitLabel: "Create",
183
- pending: false,
184
- error: null,
185
- payload: {
186
- kind: "builtin",
187
- actionId: "create-pane",
188
- },
189
- };
190
- const confirmRoute: CommandBarRoute = {
191
- kind: "confirm",
192
- confirmId: "delete",
193
- title: "Delete",
194
- body: ["Delete item?"],
195
- confirmLabel: "Delete",
196
- cancelLabel: "Back",
197
- tone: "danger",
198
- onConfirm: () => {},
199
- pending: false,
200
- error: null,
201
- };
202
-
203
- expect(getScreenFooterLeft(null)).toBe("up/down move enter select");
204
- expect(getScreenFooterLeft(orderedPickerRoute)).toBe("up/down move space toggle [ ] reorder enter done");
205
- expect(getScreenFooterRight(orderedPickerRoute)).toBe("backspace/esc back");
206
- expect(getScreenFooterRight(filteredPickerRoute)).toBe("backspace delete esc back");
207
- expect(getScreenFooterRight(workflowRoute)).toBe("backspace/esc back");
208
- expect(getScreenFooterRight(confirmRoute)).toBe("backspace/esc back");
209
- });
210
-
211
133
  test("maps route and collection command ids", () => {
212
134
  expect(isRouteCommandId("plugins")).toBe(true);
213
135
  expect(routeCommandIdToScreen("plugins")).toBe("plugins");
214
- expect(isRootParsedCommand("remove-portfolio")).toBe(true);
215
136
  expect(isCollectionCommand("remove-portfolio")).toBe(true);
216
137
  expect(getCollectionCommandKind("remove-portfolio")).toBe("portfolio");
217
138
  expect(getCollectionCommandAction("remove-portfolio")).toBe("remove");
@@ -1,4 +1,4 @@
1
- import type { CommandDef, PaneSettingField } from "../../types/plugin";
1
+ import type { CommandDef } from "../../types/plugin";
2
2
  import type { CommandBarRoute } from "./workflow/types";
3
3
  import type { CollectionKind, CollectionMembershipAction } from "./workflow/ops";
4
4
 
@@ -37,14 +37,6 @@ export function routeCommandIdToScreen(commandId: RouteCommandId): "ticker-searc
37
37
  }
38
38
  }
39
39
 
40
- export function isRootParsedCommand(commandId: string): boolean {
41
- return commandId === "security-description"
42
- || commandId === "add-watchlist"
43
- || commandId === "add-portfolio"
44
- || commandId === "remove-watchlist"
45
- || commandId === "remove-portfolio";
46
- }
47
-
48
40
  export function isCollectionCommand(commandId: string): commandId is CollectionCommandId {
49
41
  return commandId === "add-watchlist"
50
42
  || commandId === "add-portfolio"
@@ -64,33 +56,6 @@ export function getCollectionCommandVerb(action: CollectionMembershipAction): st
64
56
  return action === "add" ? "Add" : "Remove";
65
57
  }
66
58
 
67
- export function summarizePaneSettingValue(field: PaneSettingField, value: unknown): string {
68
- switch (field.type) {
69
- case "toggle":
70
- return value === true ? "On" : "Off";
71
- case "text":
72
- return typeof value === "string" && value.trim().length > 0 ? value : "Unset";
73
- case "select": {
74
- const option = field.options.find((entry) => entry.value === value);
75
- return option?.label ?? "Unset";
76
- }
77
- case "multi-select":
78
- case "ordered-multi-select": {
79
- const selected = Array.isArray(value)
80
- ? value.filter((entry): entry is string => typeof entry === "string")
81
- : [];
82
- if (selected.length === 0) return "None";
83
- const labels = selected
84
- .map((entry) => field.options.find((option) => option.value === entry)?.label ?? entry)
85
- .slice(0, 3);
86
- const suffix = selected.length > 3 ? ` +${selected.length - 3}` : "";
87
- return `${labels.join(", ")}${suffix}`;
88
- }
89
- default:
90
- return "";
91
- }
92
- }
93
-
94
59
  export function getScreenFooterLeft(route: CommandBarRoute | null): string {
95
60
  if (!route) return "up/down move enter select";
96
61
  switch (route.kind) {
@@ -3,7 +3,7 @@ import type { PluginRegistry } from "../../plugins/registry";
3
3
  import { fuzzyFilter } from "../../utils/fuzzy-search";
4
4
  import type { ResultItem } from "./list/model";
5
5
  import type { CommandBarRoute, CommandBarWorkflowRoute } from "./workflow/types";
6
- import { summarizePaneSettingValue } from "./helpers";
6
+ import { summarizePaneSettingValue } from "../pane-settings-dialog/value";
7
7
 
8
8
  type NotifyFn = (body: string, options?: { type?: "info" | "success" | "error" }) => void;
9
9
 
@@ -37,7 +37,6 @@ interface CommandBarPanelRuntimeOptions {
37
37
  applyThemePreview: (themeId: string | null) => void;
38
38
  cellHeightPx: number;
39
39
  cellWidthPx: number;
40
- clearThemePreview: (themeId: string | null | undefined) => void;
41
40
  closeAll: (options?: { revertThemePreview?: boolean }) => void;
42
41
  commitTheme: (themeId: string) => void;
43
42
  committedThemeId: string;
@@ -92,7 +91,6 @@ export function useCommandBarPanelRuntime({
92
91
  applyThemePreview,
93
92
  cellHeightPx,
94
93
  cellWidthPx,
95
- clearThemePreview,
96
94
  closeAll,
97
95
  commitTheme,
98
96
  committedThemeId,
@@ -24,17 +24,6 @@ export function normalizeCommandTickerSearchText(value: string): string {
24
24
  return value.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "");
25
25
  }
26
26
 
27
- export function isLikelyPlainTickerSearch(query: string): boolean {
28
- const trimmed = query.trim();
29
- if (!trimmed || /\s/.test(trimmed)) return false;
30
- if (!/^[A-Za-z0-9.^=\-/]+$/.test(trimmed)) return false;
31
-
32
- const compact = normalizeCommandTickerSearchText(trimmed);
33
- if (compact.length < 2) return false;
34
- if (compact.length <= 6) return true;
35
- return /[.^=\-/]/.test(trimmed) && compact.length <= 10;
36
- }
37
-
38
27
  function isExactTickerResultMatch(item: ResultItem, query: string): boolean {
39
28
  if (item.kind !== "ticker" && item.kind !== "search") return false;
40
29
  const normalizedQuery = normalizeCommandTickerSearchText(query);
@@ -81,22 +81,6 @@ function mutablePaneRegistryMap(map: ReadonlyMap<string, unknown>): Map<string,
81
81
  }
82
82
 
83
83
  describe("CommandBar", () => {
84
- test("renders the default layout with opencode-style chrome", async () => {
85
- testSetup = await testRender(<CommandBarHarness query="" selectedTicker="AAPL" />, {
86
- width: 80,
87
- height: 24,
88
- });
89
-
90
- await testSetup.renderOnce();
91
-
92
- const frame = testSetup.captureCharFrame();
93
- expect(frame).toContain("Commands");
94
- expect(frame).not.toContain("Tickers");
95
- expect(frame).toContain("Panes");
96
- expect(frame).toContain("Portfolio");
97
- expect(frame).toContain("Help");
98
- });
99
-
100
84
  test("keeps generic command filtering separate from ticker search", async () => {
101
85
  const searchQueries: string[] = [];
102
86
  testSetup = await testRender(<CommandBarHarness
@@ -291,7 +291,6 @@ export function CommandBar({
291
291
  applyThemePreview,
292
292
  cellHeightPx,
293
293
  cellWidthPx,
294
- clearThemePreview,
295
294
  closeAll,
296
295
  commitTheme,
297
296
  committedThemeId: state.config.theme,
@@ -1,148 +1,54 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { act, useState } from "react";
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { act } from "react";
3
3
  import { testRender } from "../../renderers/opentui/test-utils";
4
4
  import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app/context";
5
5
  import { createDefaultConfig } from "../../types/config";
6
- import {
7
- FeedDataTableStackView,
8
- type FeedDataTableItem,
9
- } from "./stack-view";
10
6
  import { Box, Text } from "../../ui";
7
+ import { FeedDataTableStackView } from "./stack-view";
11
8
 
12
9
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
13
- let rootKeyHits = 0;
14
-
15
- const items: FeedDataTableItem[] = [
16
- {
17
- id: "1",
18
- eyebrow: "Reuters",
19
- title:
20
- "Long headline with enough words to prove the table truncates cleanly",
21
- timestamp: new Date("2024-08-01T12:00:00Z"),
22
- preview: "Preview text with a little more color.",
23
- detailTitle: "Long headline with enough words",
24
- detailMeta: ["Reuters", "Published Aug 1, 2024"],
25
- detailBody:
26
- "This body should stay readable when the pane opens the detail page.",
27
- detailNote: "https://example.com/story",
28
- },
29
- {
30
- id: "2",
31
- eyebrow: "10-Q",
32
- title: "10-Q filing",
33
- timestamp: new Date("2024-08-02T12:00:00Z"),
34
- preview: "Quarterly report",
35
- detailTitle: "10-Q filing",
36
- detailMeta: ["Filed Aug 2, 2024"],
37
- detailBody: "Quarterly report details.",
38
- },
39
- ];
40
10
 
41
11
  afterEach(async () => {
42
- if (testSetup) {
43
- await act(async () => {
44
- testSetup!.renderer.destroy();
45
- });
46
- testSetup = undefined;
47
- }
48
- rootKeyHits = 0;
12
+ await act(async () => testSetup?.renderer.destroy());
13
+ testSetup = undefined;
49
14
  });
50
15
 
51
- function Harness({
52
- width,
53
- height,
54
- withRootControls = false,
55
- }: {
56
- width: number;
57
- height: number;
58
- withRootControls?: boolean;
59
- }) {
60
- const [selectedIdx, setSelectedIdx] = useState(0);
61
-
62
- const state = createInitialState(
63
- createDefaultConfig("/tmp/gloomberb-detail-data-table-test"),
64
- );
65
-
66
- return (
67
- <AppContext value={{ state, dispatch: () => {} }}>
68
- <PaneInstanceProvider paneId="portfolio-list:main">
69
- <FeedDataTableStackView
70
- width={width}
71
- height={height}
72
- focused
73
- items={items}
74
- selectedIdx={selectedIdx}
75
- onSelect={setSelectedIdx}
76
- rootBefore={withRootControls ? (
77
- <Box height={1}>
78
- <Text>Feed controls</Text>
79
- </Box>
80
- ) : undefined}
81
- onRootKeyDown={withRootControls ? (event) => {
82
- if (event.name !== "f") return false;
83
- rootKeyHits += 1;
84
- return true;
85
- } : undefined}
86
- sourceLabel="Source"
87
- titleLabel="Headline"
88
- />
89
- </PaneInstanceProvider>
90
- </AppContext>
91
- );
92
- }
93
-
94
- async function renderSettled() {
16
+ test("delegates root key handling", async () => {
17
+ let keyHits = 0;
18
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-feed-table-test"));
95
19
  await act(async () => {
96
- await testSetup!.renderOnce();
97
- await testSetup!.renderOnce();
20
+ testSetup = await testRender(
21
+ <AppContext value={{ state, dispatch: () => {} }}>
22
+ <PaneInstanceProvider paneId="portfolio-list:main">
23
+ <FeedDataTableStackView
24
+ width={80}
25
+ height={12}
26
+ focused
27
+ items={[]}
28
+ selectedIdx={0}
29
+ onSelect={() => {}}
30
+ rootBefore={<Box height={1}><Text>Feed controls</Text></Box>}
31
+ onRootKeyDown={(event) => {
32
+ if (event.name !== "f") return false;
33
+ keyHits += 1;
34
+ return true;
35
+ }}
36
+ />
37
+ </PaneInstanceProvider>
38
+ </AppContext>,
39
+ { width: 80, height: 12 },
40
+ );
41
+ await testSetup.renderOnce();
98
42
  });
99
- }
100
43
 
101
- async function emitKeypress(event: { name?: string; sequence?: string }) {
102
44
  await act(async () => {
103
- testSetup!.renderer.keyInput.emit("keypress", {
104
- ctrl: false,
105
- meta: false,
106
- option: false,
107
- shift: false,
108
- eventType: "press",
109
- repeated: false,
110
- preventDefault: () => {},
111
- stopPropagation: () => {},
112
- ...event,
113
- } as any);
114
- await testSetup!.renderOnce();
115
- });
116
- }
117
-
118
- describe("FeedDataTableStackView", () => {
119
- test("renders a DataTable list by default", async () => {
120
- testSetup = await testRender(<Harness width={90} height={16} />, {
121
- width: 90,
122
- height: 16,
45
+ (testSetup!.renderer.keyInput as any).emit("keypress", {
46
+ name: "f", sequence: "f", eventType: "press",
47
+ ctrl: false, meta: false, option: false, shift: false, repeated: false,
48
+ preventDefault() {}, stopPropagation() {},
123
49
  });
124
-
125
- await renderSettled();
126
- const frame = testSetup.captureCharFrame();
127
-
128
- expect(frame).toContain("Time");
129
- expect(frame).toContain("Source");
130
- expect(frame).toContain("Headline");
131
- expect(frame).toContain("10-Q filing");
132
- expect(frame).not.toContain("j/k move");
133
- expect(frame).not.toContain("Quarterly report details.");
50
+ await testSetup!.renderOnce();
134
51
  });
135
52
 
136
- test("renders root controls and delegates root key handling", async () => {
137
- testSetup = await testRender(<Harness width={90} height={16} withRootControls />, {
138
- width: 90,
139
- height: 16,
140
- });
141
-
142
- await renderSettled();
143
- expect(testSetup.captureCharFrame()).toContain("Feed controls");
144
-
145
- await emitKeypress({ name: "f", sequence: "f" });
146
- expect(rootKeyHits).toBe(1);
147
- });
53
+ expect(keyHits).toBe(1);
148
54
  });
@@ -21,7 +21,6 @@ export interface FeedDataTableItem {
21
21
  eyebrow?: string;
22
22
  title: string;
23
23
  timestamp?: Date | string | null;
24
- preview?: string | null;
25
24
  detailTitle?: string;
26
25
  detailMeta?: string[];
27
26
  detailBody?: string | null;
@@ -14,7 +14,6 @@ import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutC
14
14
  import type { PluginRegistry } from "../../../plugins/registry";
15
15
  import type { PaneProps } from "../../../types/plugin";
16
16
  import { Textarea } from "../../../ui";
17
- import { Header } from "../header";
18
17
  import {
19
18
  buildNativeWindowState,
20
19
  resolvePaneManagementShortcut,
@@ -70,7 +69,7 @@ function createShellPluginRegistry(options?: {
70
69
  } as unknown as PluginRegistry;
71
70
  }
72
71
 
73
- async function emitKeypress(event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; alt?: boolean }) {
72
+ async function emitKeypress(event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; super?: boolean; shift?: boolean; alt?: boolean }) {
74
73
  await act(async () => {
75
74
  const keyEvent = {
76
75
  ctrl: false,
@@ -184,112 +183,6 @@ function findUpdateLayout(actions: ShellTestAction[]) {
184
183
  return actions.find((action) => action.type === "UPDATE_LAYOUT");
185
184
  }
186
185
 
187
- function HeaderHarness({
188
- updateAvailable = null,
189
- updateProgress = null,
190
- updateCheckInProgress = false,
191
- updateNotice = null,
192
- }: {
193
- updateAvailable?: ReturnType<typeof createInitialState>["updateAvailable"];
194
- updateProgress?: ReturnType<typeof createInitialState>["updateProgress"];
195
- updateCheckInProgress?: boolean;
196
- updateNotice?: string | null;
197
- }) {
198
- const initialState = createInitialState(createDefaultConfig("/tmp/gloomberb-header-test"));
199
- initialState.updateAvailable = updateAvailable;
200
- initialState.updateProgress = updateProgress;
201
- initialState.updateCheckInProgress = updateCheckInProgress;
202
- initialState.updateNotice = updateNotice;
203
- const [state, dispatch] = useReducer(appReducer, initialState);
204
-
205
- return (
206
- <AppContext value={{ state, dispatch }}>
207
- <Header />
208
- </AppContext>
209
- );
210
- }
211
-
212
- describe("Header", () => {
213
- test("shows automatic self-update status for standalone binaries", async () => {
214
- testSetup = await testRender(
215
- <HeaderHarness updateAvailable={{
216
- version: "0.3.0",
217
- tagName: "v0.3.0",
218
- downloadUrl: "https://example.com/gloomberb",
219
- publishedAt: "2026-04-01T00:00:00.000Z",
220
- updateAction: { kind: "self" },
221
- }} />,
222
- { width: 120, height: 2 },
223
- );
224
-
225
- await testSetup.renderOnce();
226
- const frame = testSetup!.captureCharFrame();
227
-
228
- expect(frame).toContain("v0.3.0 available");
229
- expect(frame).toContain("starting download");
230
- expect(frame).not.toContain("press u to update");
231
- });
232
-
233
- test("shows the manual npm command when self-update is disabled", async () => {
234
- testSetup = await testRender(
235
- <HeaderHarness updateAvailable={{
236
- version: "0.3.0",
237
- tagName: "v0.3.0",
238
- downloadUrl: "https://example.com/gloomberb",
239
- publishedAt: "2026-04-01T00:00:00.000Z",
240
- updateAction: { kind: "manual", command: "npm install -g gloomberb@latest" },
241
- }} />,
242
- { width: 120, height: 2 },
243
- );
244
-
245
- await testSetup.renderOnce();
246
- const frame = testSetup!.captureCharFrame();
247
-
248
- expect(frame).toContain("v0.3.0 available");
249
- expect(frame).toContain("run npm install -g gloomberb@latest");
250
- expect(frame).not.toContain("press u to update");
251
- });
252
-
253
- test("shows update download progress and notices", async () => {
254
- testSetup = await testRender(
255
- <HeaderHarness
256
- updateAvailable={{
257
- version: "0.3.0",
258
- tagName: "v0.3.0",
259
- downloadUrl: "https://example.com/gloomberb",
260
- publishedAt: "2026-04-01T00:00:00.000Z",
261
- updateAction: { kind: "self" },
262
- }}
263
- updateProgress={{ phase: "downloading", percent: 42 }}
264
- />,
265
- { width: 120, height: 2 },
266
- );
267
-
268
- await testSetup.renderOnce();
269
- expect(testSetup.captureCharFrame()).toContain("Downloading v0.3.0: 42%");
270
-
271
- testSetup.renderer.destroy();
272
-
273
- testSetup = await testRender(
274
- <HeaderHarness updateCheckInProgress />,
275
- { width: 120, height: 2 },
276
- );
277
-
278
- await testSetup.renderOnce();
279
- expect(testSetup.captureCharFrame()).toContain("Checking for updates...");
280
-
281
- testSetup.renderer.destroy();
282
-
283
- testSetup = await testRender(
284
- <HeaderHarness updateNotice="Already on v0.3.1" />,
285
- { width: 120, height: 2 },
286
- );
287
-
288
- await testSetup.renderOnce();
289
- expect(testSetup.captureCharFrame()).toContain("Already on v0.3.1");
290
- });
291
- });
292
-
293
186
  describe("Shell", () => {
294
187
  test("uses the desktop titlebar overlay height for shell chrome math", () => {
295
188
  expect(resolveAppHeaderHeightCells({ titleBarOverlay: true, cellHeightPx: 18 })).toBe(28 / 18);
@@ -716,7 +609,6 @@ describe("Shell", () => {
716
609
  await emitKeypress({
717
610
  name: "f",
718
611
  key: "f",
719
- meta: true,
720
612
  super: true,
721
613
  shift: true,
722
614
  targetEditable: true,
@@ -1330,10 +1222,7 @@ describe("Shell", () => {
1330
1222
  );
1331
1223
 
1332
1224
  await testSetup.renderOnce();
1333
- await act(async () => {
1334
- testSetup!.mockInput.pressKey("w", { meta: true });
1335
- await testSetup!.renderOnce();
1336
- });
1225
+ await emitKeypress({ name: "w", super: true });
1337
1226
 
1338
1227
  const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
1339
1228
  expect(updateLayout?.layout.instances.map((instance: { instanceId: string }) => instance.instanceId)).toEqual(["portfolio-list:main"]);
@@ -212,7 +212,6 @@ export function Shell({
212
212
 
213
213
  const {
214
214
  dockedPanes,
215
- floatingPanes,
216
215
  paneMap,
217
216
  visibleFloatingPanes,
218
217
  } = useShellResolvedPanes({
@@ -3,7 +3,6 @@ import {
3
3
  applyDrop,
4
4
  floatPane,
5
5
  removePane,
6
- type LayoutBounds,
7
6
  type ResolvedPane,
8
7
  } from "../../../plugins/pane-manager";
9
8
  import type { PluginRegistry } from "../../../plugins/registry";
@@ -6,7 +6,6 @@ import type {
6
6
  LayoutBounds,
7
7
  ResolvedPane,
8
8
  } from "../../../plugins/pane-manager";
9
- import type { LayoutConfig } from "../../../types/config";
10
9
  import {
11
10
  constrainFloatingRectToBounds,
12
11
  pointInRect,
@@ -81,9 +81,9 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
81
81
  }
82
82
 
83
83
  useEffect(() => {
84
- if (editingField) {
85
- setTimeout(() => inputRef.current?.focus(), 10);
86
- }
84
+ if (!editingField) return;
85
+ const focusTimer = setTimeout(() => inputRef.current?.focus?.(), 10);
86
+ return () => clearTimeout(focusTimer);
87
87
  }, [editingField, portfolioSub, brokerFieldIdx]);
88
88
 
89
89
  useEffect(() => {
@@ -72,7 +72,7 @@ export function useOnboardingKeyboard({
72
72
  }
73
73
 
74
74
  if (editingField) {
75
- if (event.name === "return") {
75
+ if (event.name === "enter" || event.name === "return") {
76
76
  setEditingField(false);
77
77
  if (portfolioSub === "broker-fields" && selectedBrokerId) {
78
78
  const currentField = activeBrokerFields[brokerFieldIdx];
@@ -188,28 +188,7 @@ export function useOnboardingKeyboard({
188
188
  }
189
189
  }
190
190
  nextStep();
191
- } else if (isBackNavigationKey(event)) {
192
- if (step === "portfolio" && portfolioSub === "broker-sync") {
193
- resetBrokerSync();
194
- setPortfolioSub("broker-fields");
195
- return;
196
- }
197
- if (step === "portfolio" && portfolioSub === "broker-fields") {
198
- setPortfolioSub("broker-setup");
199
- return;
200
- }
201
- if (step === "portfolio" && portfolioSub === "broker-setup") {
202
- setPortfolioSub("broker-fields");
203
- setBrokerFieldIdx(0);
204
- return;
205
- }
206
- if (step === "portfolio" && portfolioSub !== "choose") {
207
- setPortfolioSub("choose");
208
- setBrokerFieldIdx(0);
209
- return;
210
- }
211
- prevStep();
212
- } else if (event.name === "left") {
191
+ } else if (isBackNavigationKey(event) || event.name === "left") {
213
192
  if (step === "portfolio" && portfolioSub === "broker-sync") {
214
193
  resetBrokerSync();
215
194
  setPortfolioSub("broker-fields");