gloomberb 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +106 -54
  2. package/package.json +14 -13
  3. package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
  4. package/src/api-client/chat.ts +8 -0
  5. package/src/api-client/index.test.ts +30 -0
  6. package/src/api-client/index.ts +4 -0
  7. package/src/api-client/normalizers.ts +1 -0
  8. package/src/api-client/types.ts +1 -0
  9. package/src/app/global-shortcuts.test.tsx +54 -4
  10. package/src/app/global-shortcuts.ts +11 -5
  11. package/src/capabilities/factories.ts +4 -0
  12. package/src/components/chart/comparison/data.test.ts +54 -0
  13. package/src/components/chart/comparison/data.ts +47 -24
  14. package/src/components/chart/core/projection.ts +51 -27
  15. package/src/components/command-bar/action-runtime.ts +1 -0
  16. package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
  17. package/src/components/command-bar/layout-items/current-layout.ts +33 -0
  18. package/src/components/command-bar/list/view.tsx +3 -1
  19. package/src/components/command-bar/pane-templates/items.ts +4 -1
  20. package/src/components/command-bar/panel/body.tsx +1 -0
  21. package/src/components/command-bar/routes/root/results.ts +0 -30
  22. package/src/components/command-bar/routes/root/runtime.ts +2 -20
  23. package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
  24. package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
  25. package/src/components/command-bar/routing/launch-request.ts +14 -7
  26. package/src/components/command-bar/routing/list-state.ts +1 -3
  27. package/src/components/command-bar/surface/index.test.tsx +76 -4
  28. package/src/components/command-bar/surface/index.tsx +0 -2
  29. package/src/components/command-bar/surface/pane.test.tsx +1 -0
  30. package/src/components/command-bar/surface/test-harness.tsx +1 -0
  31. package/src/components/command-bar/view-model.ts +3 -3
  32. package/src/components/data-table/stack-view.test.tsx +14 -11
  33. package/src/components/data-table/stack-view.tsx +1 -2
  34. package/src/components/data-table/view.test.tsx +24 -18
  35. package/src/components/data-table/view.tsx +313 -83
  36. package/src/components/feed-data-table/stack-view.tsx +6 -8
  37. package/src/components/layout/pane/footer/index.test.tsx +6 -0
  38. package/src/components/layout/pane/footer/index.tsx +14 -2
  39. package/src/components/layout/pane/title.ts +5 -1
  40. package/src/components/layout/shell/index.test.tsx +120 -2
  41. package/src/components/layout/shell/index.tsx +2 -0
  42. package/src/components/layout/shell/menu.ts +1 -7
  43. package/src/components/layout/shell/pane/actions.ts +8 -0
  44. package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
  45. package/src/components/layout/shell/shortcuts.ts +14 -6
  46. package/src/components/layout/shell/window-mode/index.ts +8 -3
  47. package/src/components/layout/window-edit/presentation.ts +4 -4
  48. package/src/components/onboarding/onboarding-steps.tsx +3 -2
  49. package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
  50. package/src/components/pane-settings-dialog/tui.tsx +1 -1
  51. package/src/components/ticker/list-table-view.tsx +17 -25
  52. package/src/components/ui/choice-dialog.tsx +12 -2
  53. package/src/components/ui/multi-select/dialog.tsx +17 -5
  54. package/src/components/ui/remote-image.tsx +1 -14
  55. package/src/components/ui/shortcut-hint.tsx +14 -2
  56. package/src/components/ui/table-layout.test.ts +2 -2
  57. package/src/components/ui/table-layout.ts +8 -2
  58. package/src/components/ui/tabs.tsx +115 -86
  59. package/src/components/ui/ui.test.tsx +83 -4
  60. package/src/core/state/app/types.ts +12 -4
  61. package/src/market-data/coordinator/auxiliary.ts +29 -1
  62. package/src/market-data/coordinator/index.ts +17 -1
  63. package/src/market-data/hooks.tsx +17 -1
  64. package/src/market-data/selectors.ts +4 -0
  65. package/src/plugins/builtin/account-management/form-components.tsx +0 -4
  66. package/src/plugins/builtin/account-management/model.test.ts +54 -0
  67. package/src/plugins/builtin/account-management/model.ts +13 -3
  68. package/src/plugins/builtin/account-management/pane.tsx +5 -11
  69. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  70. package/src/plugins/builtin/alerts/pane.tsx +5 -3
  71. package/src/plugins/builtin/analytics/index.tsx +0 -3
  72. package/src/plugins/builtin/analytics/view.tsx +6 -5
  73. package/src/plugins/builtin/broker-manager/index.tsx +6 -6
  74. package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
  75. package/src/plugins/builtin/changelog/index.tsx +40 -27
  76. package/src/plugins/builtin/changelog/model.test.ts +77 -0
  77. package/src/plugins/builtin/changelog/model.ts +98 -0
  78. package/src/plugins/builtin/chat/channel-labels.ts +53 -0
  79. package/src/plugins/builtin/chat/channels.test.ts +28 -0
  80. package/src/plugins/builtin/chat/channels.ts +36 -19
  81. package/src/plugins/builtin/chat/chat.test.tsx +264 -5
  82. package/src/plugins/builtin/chat/content/channel-navigation.ts +50 -11
  83. package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
  84. package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
  85. package/src/plugins/builtin/chat/content/composer.tsx +23 -2
  86. package/src/plugins/builtin/chat/content/index.tsx +66 -4
  87. package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
  88. package/src/plugins/builtin/chat/content/shortcuts.ts +27 -1
  89. package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
  90. package/src/plugins/builtin/chat/content/types.ts +1 -0
  91. package/src/plugins/builtin/chat/controller/index.ts +41 -0
  92. package/src/plugins/builtin/chat/controller/messages.ts +18 -0
  93. package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
  94. package/src/plugins/builtin/chat/controller/utils.ts +6 -3
  95. package/src/plugins/builtin/chat/controller/view.ts +1 -1
  96. package/src/plugins/builtin/chat/controller.test.ts +189 -0
  97. package/src/plugins/builtin/chat/edit-window.ts +24 -0
  98. package/src/plugins/builtin/chat/layout.ts +94 -31
  99. package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
  100. package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
  101. package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
  102. package/src/plugins/builtin/chat/message/render-state.ts +5 -1
  103. package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
  104. package/src/plugins/builtin/chat/message/types.ts +2 -0
  105. package/src/plugins/builtin/chat/pane.tsx +35 -7
  106. package/src/plugins/builtin/chat/sidebar.test.tsx +72 -0
  107. package/src/plugins/builtin/chat/status-widget.tsx +29 -11
  108. package/src/plugins/builtin/chat/test-harness.tsx +2 -0
  109. package/src/plugins/builtin/cloud/plugin.tsx +3 -0
  110. package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
  111. package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
  112. package/src/plugins/builtin/earnings/index.tsx +8 -7
  113. package/src/plugins/builtin/econ/index.tsx +6 -8
  114. package/src/plugins/builtin/help/components.tsx +1 -1
  115. package/src/plugins/builtin/help/index.test.tsx +105 -30
  116. package/src/plugins/builtin/help/index.tsx +326 -164
  117. package/src/plugins/builtin/help/shortcut-model.ts +5 -6
  118. package/src/plugins/builtin/holders/pane.tsx +7 -6
  119. package/src/plugins/builtin/market-movers/index.tsx +6 -3
  120. package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
  121. package/src/plugins/builtin/options/view.tsx +16 -8
  122. package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
  123. package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
  124. package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
  125. package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
  126. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
  127. package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
  128. package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
  129. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
  130. package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
  131. package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
  132. package/src/plugins/builtin/research/index.test.ts +30 -1
  133. package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
  134. package/src/plugins/builtin/sec/filing-documents.ts +66 -0
  135. package/src/plugins/builtin/sec/index.tsx +132 -20
  136. package/src/plugins/builtin/sectors/index.tsx +13 -23
  137. package/src/plugins/builtin/shared/ticker-request.ts +1 -0
  138. package/src/plugins/builtin/substack/api/auth.ts +185 -0
  139. package/src/plugins/builtin/substack/api/cache.ts +78 -0
  140. package/src/plugins/builtin/substack/api/loaders.ts +146 -0
  141. package/src/plugins/builtin/substack/api/store.ts +186 -0
  142. package/src/plugins/builtin/substack/api/types.ts +40 -0
  143. package/src/plugins/builtin/substack/api.test.ts +298 -0
  144. package/src/plugins/builtin/substack/article-detail.tsx +359 -0
  145. package/src/plugins/builtin/substack/article-stack.tsx +129 -0
  146. package/src/plugins/builtin/substack/content.ts +478 -0
  147. package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
  148. package/src/plugins/builtin/substack/index.tsx +49 -0
  149. package/src/plugins/builtin/substack/login-view.tsx +126 -0
  150. package/src/plugins/builtin/substack/model.test.ts +155 -0
  151. package/src/plugins/builtin/substack/normalize.ts +343 -0
  152. package/src/plugins/builtin/substack/pane-footer.ts +71 -0
  153. package/src/plugins/builtin/substack/pane-state.ts +170 -0
  154. package/src/plugins/builtin/substack/pane.tsx +500 -0
  155. package/src/plugins/builtin/substack/table.ts +111 -0
  156. package/src/plugins/builtin/substack/types.ts +71 -0
  157. package/src/plugins/builtin/substack/utils.ts +131 -0
  158. package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
  161. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
  162. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
  163. package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
  164. package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
  165. package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
  166. package/src/plugins/builtin/world-indices/index.tsx +9 -6
  167. package/src/plugins/catalog-ui.ts +2 -0
  168. package/src/plugins/pane-manager/layout-state.ts +11 -0
  169. package/src/plugins/pane-manager.ts +1 -1
  170. package/src/plugins/prediction-markets/pane.tsx +10 -9
  171. package/src/plugins/registry/index.ts +3 -0
  172. package/src/plugins/runtime/context.tsx +1 -0
  173. package/src/plugins/runtime/index.tsx +4 -0
  174. package/src/react/use-throttled-commit-value.ts +116 -0
  175. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
  176. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
  177. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  178. package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
  179. package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
  180. package/src/renderers/electrobun/view/host/input.tsx +23 -2
  181. package/src/renderers/electrobun/view/http-fetch.ts +10 -3
  182. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  183. package/src/sources/gloomberb-cloud/index.ts +4 -0
  184. package/src/sources/provider-router/cache.ts +1 -0
  185. package/src/sources/provider-router/document-routes.ts +45 -0
  186. package/src/sources/provider-router/index.ts +4 -0
  187. package/src/sources/sec-edgar/content.ts +9 -3
  188. package/src/sources/sec-edgar.test.ts +108 -1
  189. package/src/sources/sec-edgar.ts +105 -1
  190. package/src/sources/yahoo-finance.ts +4 -0
  191. package/src/state/app/context/index.test.tsx +16 -0
  192. package/src/test-support/plugin-runtime.ts +1 -0
  193. package/src/tickers/search/index.ts +1 -0
  194. package/src/types/data-provider.ts +11 -0
  195. package/src/ui/host.tsx +1 -0
  196. package/src/version.ts +1 -1
@@ -8,10 +8,6 @@ import type { TickerSearchCandidate } from "../../../../tickers/search";
8
8
  import { matchPrefix, type Command } from "../../commands/registry";
9
9
  import type { ResultItem } from "../../list/model";
10
10
  import type { CommandBarRoute } from "../../workflow/types";
11
- import {
12
- isLikelyPlainTickerSearch,
13
- normalizeCommandTickerSearchText,
14
- } from "../ticker-search/results";
15
11
  import { useTickerSearchRouteResults } from "../ticker-search/route";
16
12
  import { buildRootResultModel, type RootResultModel } from "./results";
17
13
  import { useRootProviderSearch } from "./provider-search";
@@ -47,7 +43,6 @@ interface UseCommandBarRootRuntimeOptions {
47
43
  getTickers(): AppState["tickers"];
48
44
  hasPaneSettings(paneId: string): boolean;
49
45
  localTickerSearchResultItems(query?: string, options?: { category?: string; limit?: number }): ResultItem[];
50
- mapTickerSearchCandidateToResultItem(candidate: TickerSearchCandidate): ResultItem;
51
46
  nativeListScrollRef: RefObject<ScrollBoxRenderable | null>;
52
47
  nonShortcutPaneTemplateItems(filterQuery?: string): ResultItem[];
53
48
  openModeRoute(screen: "ticker-search" | "plugins" | "layout", initialQuery?: string): void;
@@ -101,7 +96,6 @@ export function useCommandBarRootRuntime({
101
96
  getTickers,
102
97
  hasPaneSettings,
103
98
  localTickerSearchResultItems,
104
- mapTickerSearchCandidateToResultItem,
105
99
  nativeListScrollRef,
106
100
  nonShortcutPaneTemplateItems,
107
101
  openModeRoute,
@@ -178,7 +172,6 @@ export function useCommandBarRootRuntime({
178
172
  getAvailablePaneShortcutTemplates,
179
173
  hasPaneSettings,
180
174
  localTickerSearchResultItems,
181
- mapTickerSearchCandidateToResultItem,
182
175
  nonShortcutPaneTemplateItems,
183
176
  openModeRoute,
184
177
  paneShortcutItems,
@@ -206,7 +199,6 @@ export function useCommandBarRootRuntime({
206
199
  getAvailablePaneShortcutTemplates,
207
200
  hasPaneSettings,
208
201
  localTickerSearchResultItems,
209
- mapTickerSearchCandidateToResultItem,
210
202
  nonShortcutPaneTemplateItems,
211
203
  openModeRoute,
212
204
  paneShortcutItems,
@@ -220,20 +212,10 @@ export function useCommandBarRootRuntime({
220
212
  tickerActionItems,
221
213
  ]);
222
214
 
223
- const rootPlainTickerSearchArg = useMemo(() => {
224
- if (activeMatch || rootShortcutIntent.kind !== "none" || !isLikelyPlainTickerSearch(rootQuery)) return null;
225
- const normalizedQuery = normalizeCommandTickerSearchText(rootQuery);
226
- const hasExactNonTickerResult = rootResultModel.items.some((item) => (
227
- item.kind !== "ticker"
228
- && item.kind !== "search"
229
- && normalizeCommandTickerSearchText(item.label) === normalizedQuery
230
- ));
231
- return hasExactNonTickerResult ? null : rootQuery.trim();
232
- }, [activeMatch, rootQuery, rootResultModel.items, rootShortcutIntent.kind]);
233
215
  const rootSecurityDescriptionArg = activeMatch?.command.id === "security-description" && activeMatch.arg.length >= 1
234
216
  ? activeMatch.arg
235
217
  : null;
236
- const rootTickerSearchArg = rootSecurityDescriptionArg ?? rootPlainTickerSearchArg;
218
+ const rootTickerSearchArg = rootSecurityDescriptionArg;
237
219
 
238
220
  useEffect(() => {
239
221
  if (currentRoute) return;
@@ -272,7 +254,7 @@ export function useCommandBarRootRuntime({
272
254
  localTickerSearchResultItems,
273
255
  portfolios: state.config.portfolios,
274
256
  readTickerSearchCache,
275
- rootPlainTickerSearchArg,
257
+ rootPlainTickerSearchArg: null,
276
258
  rootResultItems: rootResultModel.items,
277
259
  rootTickerSearchArg,
278
260
  tickers: state.tickers,
@@ -6,6 +6,7 @@ import {
6
6
  rankTickerSearchItems,
7
7
  upsertTickerFromSearchResult,
8
8
  type TickerSearchCandidate,
9
+ type TickerSearchInstrumentClass,
9
10
  } from "../../../../tickers/search";
10
11
  import type { ResultItem } from "../../list/model";
11
12
  import {
@@ -45,12 +46,15 @@ export function useCommandBarTickerSearchActions({
45
46
  }, [tickerRepository, dispatch, pluginRegistry.events, focusTicker, closeAll]);
46
47
 
47
48
  const mapTickerSearchCandidateToResultItem = useCallback((candidate: TickerSearchCandidate): ResultItem => {
49
+ const detail = formatTickerSearchDetail(candidate);
50
+ const right = formatTickerSearchRight(candidate);
51
+
48
52
  if (candidate.kind === "ticker" && candidate.ticker) {
49
53
  return {
50
54
  id: candidate.id,
51
55
  label: candidate.label,
52
- detail: candidate.detail,
53
- right: candidate.right,
56
+ detail,
57
+ right,
54
58
  category: candidate.category,
55
59
  kind: "ticker",
56
60
  secondaryAction: () => {
@@ -67,8 +71,8 @@ export function useCommandBarTickerSearchActions({
67
71
  return {
68
72
  id: candidate.id,
69
73
  label: candidate.label,
70
- detail: candidate.detail,
71
- right: candidate.right,
74
+ detail,
75
+ right,
72
76
  category: candidate.category,
73
77
  kind: "search",
74
78
  secondaryAction: () => openTickerResearch(candidate.result!, { forceNewPane: true }),
@@ -140,3 +144,37 @@ export function useCommandBarTickerSearchActions({
140
144
  writeTickerSearchCache,
141
145
  };
142
146
  }
147
+
148
+ function formatTickerSearchRight(candidate: TickerSearchCandidate): string | undefined {
149
+ const assetClass = formatInstrumentClass(candidate.instrumentClass);
150
+ const exchange = candidate.exchangeLabel || candidate.primaryExchangeLabel || candidate.right || "";
151
+ if (!exchange) return assetClass;
152
+ if (assetClass === "Other") return exchange;
153
+ return `${assetClass} ${exchange}`;
154
+ }
155
+
156
+ function formatTickerSearchDetail(candidate: TickerSearchCandidate): string {
157
+ const metadata = [
158
+ formatInstrumentClass(candidate.instrumentClass),
159
+ candidate.exchangeLabel,
160
+ candidate.primaryExchangeLabel && candidate.primaryExchangeLabel !== candidate.exchangeLabel
161
+ ? candidate.primaryExchangeLabel
162
+ : null,
163
+ ].filter((value): value is string => !!value);
164
+ return metadata.length > 0
165
+ ? `${candidate.detail} | ${metadata.join(" | ")}`
166
+ : candidate.detail;
167
+ }
168
+
169
+ function formatInstrumentClass(instrumentClass: TickerSearchInstrumentClass): string {
170
+ switch (instrumentClass) {
171
+ case "equity":
172
+ return "Equity";
173
+ case "fund":
174
+ return "Fund";
175
+ case "derivative":
176
+ return "Derivative";
177
+ case "other":
178
+ return "Other";
179
+ }
180
+ }
@@ -65,7 +65,10 @@ export function useTickerSearchRouteResults(options: {
65
65
 
66
66
  const searchQuery = routeQuery.trim();
67
67
  if (!searchQuery) {
68
- resetTickerSearchRoute();
68
+ searchRequestIdRef.current += 1;
69
+ setTickerSearchPending(false);
70
+ setTickerSearchResults(localTickerSearchResultItems(undefined, { limit: 8 }));
71
+ if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
69
72
  return;
70
73
  }
71
74
 
@@ -1,17 +1,17 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  import type { PluginRegistry } from "../../../plugins/registry";
3
+ import type { AppState } from "../../../state/app/context";
3
4
  import type { CommandDef } from "../../../types/plugin";
4
5
 
5
- interface CommandBarPluginLaunchRequest {
6
- kind: "plugin-command";
7
- commandId: string;
8
- sequence: number;
9
- }
10
-
11
6
  interface UseCommandBarLaunchRequestOptions {
12
7
  activeTickerSymbol: string | null;
13
- commandBarLaunchRequest: CommandBarPluginLaunchRequest | null;
8
+ commandBarLaunchRequest: AppState["commandBarLaunchRequest"];
14
9
  commandBarOpen: boolean;
10
+ openModeRoute: (
11
+ screen: "ticker-search" | "plugins" | "layout",
12
+ initialQuery?: string,
13
+ payload?: Record<string, unknown>,
14
+ ) => void;
15
15
  openPluginCommandWorkflow: (
16
16
  command: CommandDef,
17
17
  options?: { values?: Record<string, string> },
@@ -23,6 +23,7 @@ export function useCommandBarLaunchRequest({
23
23
  activeTickerSymbol,
24
24
  commandBarLaunchRequest,
25
25
  commandBarOpen,
26
+ openModeRoute,
26
27
  openPluginCommandWorkflow,
27
28
  pluginRegistry,
28
29
  }: UseCommandBarLaunchRequestOptions) {
@@ -38,6 +39,11 @@ export function useCommandBarLaunchRequest({
38
39
  if (processedLaunchSequenceRef.current === launch.sequence) return;
39
40
  processedLaunchSequenceRef.current = launch.sequence;
40
41
 
42
+ if (launch.kind === "ticker-search") {
43
+ openModeRoute("ticker-search", launch.query ?? "");
44
+ return;
45
+ }
46
+
41
47
  const command = pluginRegistry.commands.get(launch.commandId);
42
48
  if (!command?.wizard || command.wizard.length === 0) return;
43
49
  let values: Record<string, string> | undefined;
@@ -53,6 +59,7 @@ export function useCommandBarLaunchRequest({
53
59
  activeTickerSymbol,
54
60
  commandBarLaunchRequest,
55
61
  commandBarOpen,
62
+ openModeRoute,
56
63
  openPluginCommandWorkflow,
57
64
  pluginRegistry,
58
65
  ]);
@@ -125,9 +125,7 @@ function buildRouteListState(options: BuildRouteListStateOptions): ListScreenSta
125
125
  };
126
126
  }
127
127
  case "ticker-search": {
128
- const results = currentRoute.query.trim()
129
- ? tickerSearchResults.map((item) => adaptTickerSearchRouteResult(item, currentRoute.payload))
130
- : [];
128
+ const results = tickerSearchResults.map((item) => adaptTickerSearchRouteResult(item, currentRoute.payload));
131
129
  const emptyState = getEmptyState("search", currentRoute.query, currentRoute.query);
132
130
  return {
133
131
  kind: "mode",
@@ -81,13 +81,35 @@ describe("CommandBar", () => {
81
81
 
82
82
  const frame = testSetup.captureCharFrame();
83
83
  expect(frame).toContain("Commands");
84
- expect(frame).toContain("Tickers");
85
- expect(frame).toContain("AAPL");
84
+ expect(frame).not.toContain("Tickers");
86
85
  expect(frame).toContain("Panes");
87
86
  expect(frame).toContain("Portfolio");
88
87
  expect(frame).toContain("Help");
89
88
  });
90
89
 
90
+ test("keeps generic command filtering separate from ticker search", async () => {
91
+ const searchQueries: string[] = [];
92
+ testSetup = await testRender(<CommandBarHarness
93
+ query="MSFT"
94
+ dataProvider={makeDataProvider(async (query) => {
95
+ searchQueries.push(query);
96
+ return [];
97
+ })}
98
+ />, {
99
+ width: 80,
100
+ height: 24,
101
+ });
102
+
103
+ await testSetup.renderOnce();
104
+ await Bun.sleep(260);
105
+ await testSetup.renderOnce();
106
+
107
+ const frame = testSetup.captureCharFrame();
108
+ expect(frame).not.toContain("Tickers");
109
+ expect(frame).not.toContain("Microsoft Corp.");
110
+ expect(searchQueries).toEqual([]);
111
+ });
112
+
91
113
  test("runs check for updates from the command bar", async () => {
92
114
  const calls: number[] = [];
93
115
 
@@ -416,6 +438,56 @@ describe("CommandBar", () => {
416
438
  expect(frame).toContain("AMD");
417
439
  });
418
440
 
441
+ test("opens ticker search from a launch request with saved ticker metadata", async () => {
442
+ testSetup = await testRender(<CommandBarHarness
443
+ query=""
444
+ extraTickers={[makeTicker("BRK.B", "Berkshire Hathaway Inc.", {
445
+ exchange: "NYSE",
446
+ assetCategory: "STK",
447
+ })]}
448
+ configureState={(state) => ({
449
+ ...state,
450
+ commandBarLaunchRequest: {
451
+ kind: "ticker-search",
452
+ query: "",
453
+ sequence: 1,
454
+ },
455
+ })}
456
+ />, {
457
+ width: 100,
458
+ height: 24,
459
+ });
460
+
461
+ const frame = await waitForFrameToContain("Security Description");
462
+ expectSingleBackControl(frame);
463
+ expect(frame).toContain("BRK.B");
464
+ expect(frame).toContain("Equity NYSE");
465
+ });
466
+
467
+ test("opens ticker search when activating the Ticker Research pane item without a ticker", async () => {
468
+ testSetup = await testRender(<CommandBarHarness query="ticker research" />, {
469
+ width: 100,
470
+ height: 24,
471
+ });
472
+
473
+ await testSetup.renderOnce();
474
+ const rootFrame = testSetup.captureCharFrame();
475
+ const tickerResearchRow = rootFrame
476
+ .split("\n")
477
+ .find((line) => line.includes("Ticker Research"));
478
+ expect(tickerResearchRow).toMatch(/\bT\s*$/);
479
+
480
+ await act(async () => {
481
+ testSetup!.mockInput.pressEnter();
482
+ await testSetup!.renderOnce();
483
+ });
484
+
485
+ const frame = testSetup.captureCharFrame();
486
+ expectSingleBackControl(frame);
487
+ expect(frame).toContain("Security Description");
488
+ expect(frame).toContain("Search tickers");
489
+ });
490
+
419
491
  test("keeps typed prefixes in the root query until a result is activated", async () => {
420
492
  testSetup = await testRender(<CommandBarHarness query="DES " />, {
421
493
  width: 80,
@@ -763,8 +835,8 @@ describe("CommandBar", () => {
763
835
  const rows = frame.split("\n");
764
836
  const savedHeadings = frame.split("\n").filter((line) => line.trim() === "Saved");
765
837
  const otherListingsHeadings = frame.split("\n").filter((line) => line.trim() === "Other Listings");
766
- const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL") && line.includes("NASDAQ"));
767
- const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP") && line.includes("NASDAQ"));
838
+ const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL"));
839
+ const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP"));
768
840
  expect(savedHeadings).toHaveLength(1);
769
841
  expect(otherListingsHeadings).toHaveLength(1);
770
842
  expect(aaplRow).toBeGreaterThanOrEqual(0);
@@ -110,7 +110,6 @@ export function CommandBar({
110
110
  getWorkflowFieldStringValue,
111
111
  getWorkflowInputRef,
112
112
  localTickerSearchResultItems,
113
- mapTickerSearchCandidateToResultItem,
114
113
  moveWorkflowFocus,
115
114
  nonShortcutPaneTemplateItems,
116
115
  openInlineConfirm,
@@ -193,7 +192,6 @@ export function CommandBar({
193
192
  getTickers: getTickerSearchTickers,
194
193
  hasPaneSettings,
195
194
  localTickerSearchResultItems,
196
- mapTickerSearchCandidateToResultItem,
197
195
  nativeListScrollRef,
198
196
  nonShortcutPaneTemplateItems,
199
197
  openModeRoute,
@@ -158,6 +158,7 @@ describe("CommandBar pane and layout routes", () => {
158
158
  expect(frame).toContain("Float Pane");
159
159
  expect(frame).toContain("Undo Layout Change");
160
160
  expect(frame).toContain("Current Layout");
161
+ expect(frame).toContain("Close All Floating Panes");
161
162
  });
162
163
 
163
164
  test("runs layout actions directly from root search", async () => {
@@ -174,6 +174,7 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
174
174
  paneId: TICKER_RESEARCH_PANE_ID,
175
175
  label: "Ticker Research",
176
176
  description: "Open another research pane",
177
+ shortcut: { prefix: "T", argPlaceholder: "ticker", argKind: "ticker" },
177
178
  }],
178
179
  ["new-chat-pane", {
179
180
  id: "new-chat-pane",
@@ -40,7 +40,7 @@ export function resolveCommandBarMode(query: string, commandList?: Command[]): C
40
40
  const match = matchPrefix(query, commandList);
41
41
 
42
42
  if (!query.trim()) {
43
- return { kind: "default", badge: "BROWSE", hint: "Type a command, ticker, or prefix" };
43
+ return { kind: "default", badge: "BROWSE", hint: "Type a command or prefix" };
44
44
  }
45
45
 
46
46
  if (!match) {
@@ -100,9 +100,9 @@ export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?:
100
100
  return { label: "No layout actions match", detail: query.trim() || "Focused-pane and layout actions will appear here" };
101
101
  default:
102
102
  if (query.trim()) {
103
- return { label: `No matches for "${query.trim()}"`, detail: "Try a ticker, command name, or prefix" };
103
+ return { label: `No matches for "${query.trim()}"`, detail: "Try a command name or prefix. Use T for ticker search" };
104
104
  }
105
- return { label: "No results yet", detail: "Recent tickers and suggested commands will appear here" };
105
+ return { label: "No results yet", detail: "Suggested commands will appear here" };
106
106
  }
107
107
  }
108
108
 
@@ -67,18 +67,18 @@ function Harness() {
67
67
  )
68
68
  }
69
69
  detailTitle={openRow?.title}
70
- selectedIndex={selectedIndex}
71
- onSelectIndex={(index) => setSelectedIndex(index)}
72
- onActivateIndex={(_index, row) => setOpenRow(row)}
70
+ selection={{
71
+ kind: "index",
72
+ selectedIndex,
73
+ onChange: (index) => setSelectedIndex(index),
74
+ }}
75
+ onActivate={(row) => setOpenRow(row)}
73
76
  columns={columns}
74
77
  items={rows}
75
78
  sortColumnId={null}
76
79
  sortDirection="asc"
77
80
  onHeaderClick={() => {}}
78
81
  getItemKey={(row) => row.id}
79
- isSelected={(_row, index) => index === selectedIndex}
80
- onSelect={(_row, index) => setSelectedIndex(index)}
81
- onActivate={(row) => setOpenRow(row)}
82
82
  renderCell={(row): DataTableCell => ({ text: row.title })}
83
83
  emptyStateTitle="No rows"
84
84
  showHorizontalScrollbar={false}
@@ -90,13 +90,14 @@ function Harness() {
90
90
 
91
91
  function GroupedHarness() {
92
92
  const [selectedIndex, setSelectedIndex] = useState(0);
93
+ const [cursorIndex, setCursorIndex] = useState(0);
93
94
  const state = createInitialState(
94
95
  createDefaultConfig("/tmp/gloomberb-data-table-stack-view-grouped-test"),
95
96
  );
96
97
  const columns: Column[] = [
97
98
  { id: "title", label: "Title", width: 20, align: "left" },
98
99
  ];
99
- const selectedTitle = groupedRows[selectedIndex]?.title ?? "none";
100
+ const selectedTitle = groupedRows[cursorIndex]?.title ?? "none";
100
101
 
101
102
  return (
102
103
  <AppContext value={{ state, dispatch: () => {} }}>
@@ -106,8 +107,12 @@ function GroupedHarness() {
106
107
  detailOpen={false}
107
108
  onBack={() => {}}
108
109
  detailContent={<Box flexGrow={1} />}
109
- selectedIndex={selectedIndex}
110
- onSelectIndex={(index) => setSelectedIndex(index)}
110
+ selection={{
111
+ kind: "index",
112
+ selectedIndex,
113
+ onChange: (index) => setSelectedIndex(index),
114
+ }}
115
+ onCursorChange={(_row, index) => setCursorIndex(index)}
111
116
  isNavigable={(row) => row.type !== "section"}
112
117
  rootAfter={
113
118
  <Box height={1}>
@@ -120,8 +125,6 @@ function GroupedHarness() {
120
125
  sortDirection="asc"
121
126
  onHeaderClick={() => {}}
122
127
  getItemKey={(row) => row.id}
123
- isSelected={(_row, index) => index === selectedIndex}
124
- onSelect={(_row, index) => setSelectedIndex(index)}
125
128
  renderSectionHeader={(row) => row.type === "section"
126
129
  ? { text: row.title }
127
130
  : null}
@@ -10,13 +10,12 @@ import { PageStackView, type DataTableColumn } from "../ui";
10
10
  export interface DataTableStackViewProps<
11
11
  T,
12
12
  C extends DataTableColumn = DataTableColumn,
13
- > extends Omit<DataTableViewProps<T, C>, "focused" | "selectedIndex"> {
13
+ > extends Omit<DataTableViewProps<T, C>, "focused"> {
14
14
  focused: boolean;
15
15
  detailOpen: boolean;
16
16
  onBack: () => void;
17
17
  detailContent: ReactNode;
18
18
  detailTitle?: string;
19
- selectedIndex: number;
20
19
  onDetailKeyDown?: (event: DataTableKeyEvent) => boolean | void;
21
20
  }
22
21
 
@@ -45,21 +45,27 @@ afterEach(async () => {
45
45
 
46
46
  function Harness() {
47
47
  const [selectedIndex, setSelectedIndex] = useState(1);
48
+ const [cursorIndex, setCursorIndex] = useState(1);
48
49
  const [activatedTitle, setActivatedTitle] = useState("");
49
50
  const state = createInitialState(
50
51
  createDefaultConfig("/tmp/gloomberb-data-table-view-test"),
51
52
  );
52
53
  const selectedTitle = rows[selectedIndex]?.title ?? "none";
54
+ const cursorTitle = rows[cursorIndex]?.title ?? "none";
53
55
 
54
56
  return (
55
57
  <AppContext value={{ state, dispatch: () => {} }}>
56
58
  <PaneInstanceProvider paneId="data-table-view-test">
57
59
  <DataTableView<Row, Column>
58
60
  focused
59
- selectedIndex={selectedIndex}
60
61
  isNavigable={(row) => row.type === "row"}
61
- onSelectIndex={(index) => setSelectedIndex(index)}
62
- onActivateIndex={(_index, row) => {
62
+ selection={{
63
+ kind: "index",
64
+ selectedIndex,
65
+ onChange: (index) => setSelectedIndex(index),
66
+ }}
67
+ onCursorChange={(_row, index) => setCursorIndex(index)}
68
+ onActivate={(row) => {
63
69
  if (row.type === "row") setActivatedTitle(row.title);
64
70
  }}
65
71
  columns={columns}
@@ -68,8 +74,6 @@ function Harness() {
68
74
  sortDirection="asc"
69
75
  onHeaderClick={() => {}}
70
76
  getItemKey={(row) => row.id}
71
- isSelected={(_row, index) => index === selectedIndex}
72
- onSelect={(_row, index) => setSelectedIndex(index)}
73
77
  renderSectionHeader={(row) => row.type === "section"
74
78
  ? { text: row.title }
75
79
  : null}
@@ -79,7 +83,7 @@ function Harness() {
79
83
  emptyStateTitle="No rows"
80
84
  rootAfter={
81
85
  <Box height={1}>
82
- <Text>{`selected=${selectedTitle} activated=${activatedTitle}`}</Text>
86
+ <Text>{`cursor=${cursorTitle} selected=${selectedTitle} activated=${activatedTitle}`}</Text>
83
87
  </Box>
84
88
  }
85
89
  />
@@ -102,19 +106,21 @@ function LargeSelectionHarness({
102
106
  <PaneInstanceProvider paneId="data-table-view-large-test">
103
107
  <DataTableView<Row, Column>
104
108
  focused
105
- selectedIndex={500}
109
+ selection={{
110
+ kind: "index",
111
+ selectedIndex: 500,
112
+ onChange: () => {},
113
+ }}
106
114
  columns={columns}
107
115
  items={largeRows}
108
116
  sortColumnId={null}
109
117
  sortDirection="asc"
110
118
  onHeaderClick={() => {}}
111
119
  getItemKey={(row) => row.id}
112
- isSelected={(_row, index) => {
120
+ renderCell={(row, _column, index): DataTableCell => {
113
121
  onIsSelected();
114
- return index === 500;
122
+ return { text: row.title + (index === 500 ? "" : "") };
115
123
  }}
116
- onSelect={() => {}}
117
- renderCell={(row): DataTableCell => ({ text: row.title })}
118
124
  emptyStateTitle="No rows"
119
125
  />
120
126
  </PaneInstanceProvider>
@@ -190,24 +196,24 @@ describe("DataTableView", () => {
190
196
  testSetup = await testRender(<Harness />, { width: 60, height: 12 });
191
197
 
192
198
  await renderSettled();
193
- expect(testSetup.captureCharFrame()).toContain("selected=First row");
199
+ expect(testSetup.captureCharFrame()).toContain("cursor=First row selected=First row");
194
200
 
195
201
  await emitKeypress({ name: "down", sequence: "\u001B[B" });
196
202
  await renderSettled();
197
- expect(testSetup.captureCharFrame()).toContain("selected=Second row");
203
+ expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row");
198
204
 
199
205
  await emitKeypress({ name: "up", sequence: "\u001B[A", meta: true });
200
206
  await renderSettled();
201
- expect(testSetup.captureCharFrame()).toContain("selected=Second row");
207
+ expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row");
202
208
 
203
209
  await emitKeypress({ name: "up", sequence: "\u001B[A" });
204
210
  await renderSettled();
205
- expect(testSetup.captureCharFrame()).toContain("selected=First row");
211
+ expect(testSetup.captureCharFrame()).toContain("cursor=First row selected=First row");
206
212
 
207
213
  await emitKeypress({ name: "j", sequence: "j" });
208
214
  await emitKeypress({ name: "k", sequence: "k" });
209
215
  await renderSettled();
210
- expect(testSetup.captureCharFrame()).toContain("selected=First row");
216
+ expect(testSetup.captureCharFrame()).toContain("cursor=First row selected=First row");
211
217
 
212
218
  await emitKeypress({ name: "enter", sequence: "\r" });
213
219
  await renderSettled();
@@ -215,11 +221,11 @@ describe("DataTableView", () => {
215
221
 
216
222
  await emitKeypress({ name: "j", sequence: "j" });
217
223
  await renderSettled();
218
- expect(testSetup.captureCharFrame()).toContain("selected=Second row activated=First row");
224
+ expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row activated=First row");
219
225
 
220
226
  await emitKeypress({ name: "enter", sequence: "\r", defaultPrevented: true });
221
227
  await renderSettled();
222
- expect(testSetup.captureCharFrame()).toContain("selected=Second row activated=First row");
228
+ expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row activated=First row");
223
229
  });
224
230
 
225
231
  test("keeps selection current across repeated keypresses before the next render", async () => {