gloomberb 0.6.1 → 0.7.1

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 (208) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +158 -57
  4. package/src/capabilities/factories.ts +3 -0
  5. package/src/cli/registry.test.ts +0 -1
  6. package/src/components/chart/chart-controller.ts +0 -1
  7. package/src/components/chart/chart-data.ts +1 -31
  8. package/src/components/chart/chart-renderer.ts +94 -12
  9. package/src/components/chart/chart-resolution.test.ts +0 -36
  10. package/src/components/chart/chart-resolution.ts +0 -27
  11. package/src/components/chart/chart.test.ts +60 -45
  12. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  13. package/src/components/chart/comparison-chart-data.ts +1 -4
  14. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  15. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  16. package/src/components/chart/comparison-stock-chart.tsx +63 -39
  17. package/src/components/chart/price-axis-labels.tsx +114 -0
  18. package/src/components/chart/static-chart-surface.tsx +30 -4
  19. package/src/components/chart/stock-chart.test.ts +0 -16
  20. package/src/components/chart/stock-chart.tsx +69 -41
  21. package/src/components/chart/time-axis-label.tsx +120 -0
  22. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  23. package/src/components/command-bar/command-bar.test.tsx +182 -36
  24. package/src/components/command-bar/command-bar.tsx +943 -1016
  25. package/src/components/command-bar/command-registry.ts +0 -10
  26. package/src/components/command-bar/confirm-body.tsx +51 -0
  27. package/src/components/command-bar/helpers.ts +2 -5
  28. package/src/components/command-bar/list-model.ts +76 -0
  29. package/src/components/command-bar/theme-picker.tsx +254 -0
  30. package/src/components/command-bar/view-model.test.ts +4 -4
  31. package/src/components/command-bar/view-model.ts +1 -20
  32. package/src/components/command-bar/workflow-ops.ts +4 -0
  33. package/src/components/command-bar/workflow-types.ts +1 -2
  34. package/src/components/data-table-stack-view.test.tsx +39 -4
  35. package/src/components/data-table-stack-view.tsx +11 -104
  36. package/src/components/data-table-view.tsx +43 -79
  37. package/src/components/feed-data-table-stack-view.tsx +10 -17
  38. package/src/components/feed-stack-controller.ts +39 -0
  39. package/src/components/index.ts +5 -1
  40. package/src/components/layout/detached-pane-shell.tsx +29 -10
  41. package/src/components/layout/floating-pane.tsx +35 -4
  42. package/src/components/layout/header.tsx +12 -7
  43. package/src/components/layout/pane-content.test.tsx +64 -0
  44. package/src/components/layout/pane-content.tsx +2 -0
  45. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  46. package/src/components/layout/pane-cycle-order.ts +32 -0
  47. package/src/components/layout/pane-footer.tsx +11 -4
  48. package/src/components/layout/pane-header.tsx +5 -5
  49. package/src/components/layout/pane-sizing.ts +9 -2
  50. package/src/components/layout/pane-title.ts +4 -1
  51. package/src/components/layout/pane.tsx +11 -5
  52. package/src/components/layout/shell.test.tsx +264 -22
  53. package/src/components/layout/shell.tsx +46 -8
  54. package/src/components/layout/status-bar.tsx +2 -0
  55. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  56. package/src/components/pane-settings-dialog.tsx +12 -4
  57. package/src/components/pane-template-wizard.test.tsx +3 -4
  58. package/src/components/speedometer-gauge.test.tsx +45 -0
  59. package/src/components/speedometer-gauge.tsx +562 -0
  60. package/src/components/table-view-shared.tsx +218 -0
  61. package/src/components/ticker-list-table-view.tsx +57 -73
  62. package/src/components/ticker-list-table.test.tsx +82 -1
  63. package/src/components/ticker-list-table.tsx +22 -9
  64. package/src/components/ui/confirm-dialog.tsx +66 -0
  65. package/src/components/ui/data-table.tsx +30 -19
  66. package/src/components/ui/frame.tsx +2 -0
  67. package/src/components/ui/index.ts +0 -1
  68. package/src/components/ui/page-stack-view.tsx +39 -10
  69. package/src/components/ui/table-layout.test.ts +42 -0
  70. package/src/components/ui/table-layout.ts +49 -4
  71. package/src/components/ui/tabs.tsx +34 -1
  72. package/src/components/ui/ui.test.tsx +168 -21
  73. package/src/core/state/app-state.ts +10 -17
  74. package/src/market-data/coordinator.test.ts +76 -1
  75. package/src/market-data/coordinator.ts +218 -49
  76. package/src/news/aggregator.test.ts +49 -1
  77. package/src/news/aggregator.ts +48 -3
  78. package/src/news/hooks.ts +6 -2
  79. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  80. package/src/plugins/builtin/ai/providers.ts +0 -17
  81. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  82. package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
  83. package/src/plugins/builtin/analytics/index.tsx +1 -18
  84. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  85. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  86. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  87. package/src/plugins/builtin/chat-controller.test.ts +188 -1
  88. package/src/plugins/builtin/chat-controller.ts +537 -220
  89. package/src/plugins/builtin/chat.test.tsx +431 -13
  90. package/src/plugins/builtin/chat.tsx +1134 -227
  91. package/src/plugins/builtin/cloud-tweets.tsx +1 -1
  92. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  93. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  94. package/src/plugins/builtin/correlation/index.tsx +1 -6
  95. package/src/plugins/builtin/debug.tsx +1 -3
  96. package/src/plugins/builtin/econ/index.tsx +1 -2
  97. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  98. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  99. package/src/plugins/builtin/fear-greed/index.tsx +587 -0
  100. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  101. package/src/plugins/builtin/help.test.tsx +2 -2
  102. package/src/plugins/builtin/help.tsx +2 -2
  103. package/src/plugins/builtin/holders/index.test.ts +13 -1
  104. package/src/plugins/builtin/holders/index.tsx +64 -29
  105. package/src/plugins/builtin/insider/index.tsx +22 -89
  106. package/src/plugins/builtin/market-movers/index.tsx +13 -19
  107. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  108. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  109. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  110. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  111. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  112. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  113. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  114. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  115. package/src/plugins/builtin/news.tsx +4 -16
  116. package/src/plugins/builtin/notes.tsx +19 -55
  117. package/src/plugins/builtin/options.test.tsx +58 -2
  118. package/src/plugins/builtin/options.tsx +110 -65
  119. package/src/plugins/builtin/plugin-groups.ts +2 -0
  120. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  121. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  122. package/src/plugins/builtin/research/index.test.ts +94 -0
  123. package/src/plugins/builtin/research/index.tsx +164 -33
  124. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  125. package/src/plugins/builtin/sec.tsx +13 -66
  126. package/src/plugins/builtin/sectors/index.tsx +1 -1
  127. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  128. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  129. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  130. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  131. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  132. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  133. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  134. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  135. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  136. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  137. package/src/plugins/ibkr/gateway-service.ts +1 -1
  138. package/src/plugins/ibkr/index.test.tsx +0 -2
  139. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  140. package/src/plugins/pane-manager.ts +0 -13
  141. package/src/plugins/plugin-runtime.test.tsx +40 -6
  142. package/src/plugins/plugin-runtime.tsx +20 -10
  143. package/src/plugins/prediction-markets/cli.ts +0 -1
  144. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  145. package/src/plugins/prediction-markets/controller.ts +0 -15
  146. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  147. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  148. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  149. package/src/plugins/registry.ts +13 -42
  150. package/src/plugins/ticker-navigation.test.ts +66 -0
  151. package/src/plugins/ticker-navigation.ts +44 -0
  152. package/src/react/input.ts +2 -1
  153. package/src/renderers/electrobun/bun/index.ts +32 -11
  154. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  155. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  156. package/src/renderers/electrobun/view/app-services.ts +88 -31
  157. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  158. package/src/renderers/electrobun/view/data-table.tsx +138 -101
  159. package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
  160. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  161. package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
  162. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  163. package/src/renderers/electrobun/view/key-event.ts +1 -1
  164. package/src/renderers/electrobun/view/main.tsx +18 -1
  165. package/src/renderers/electrobun/view/styles.css +329 -0
  166. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  167. package/src/renderers/electrobun/view/ui-host.tsx +42 -0
  168. package/src/renderers/opentui/chart-surface.tsx +179 -0
  169. package/src/renderers/opentui/input-host.tsx +1 -1
  170. package/src/renderers/opentui/start.tsx +48 -30
  171. package/src/renderers/opentui/test-utils.tsx +17 -0
  172. package/src/renderers/opentui/ui-host.tsx +5 -2
  173. package/src/sources/gloomberb-cloud.test.ts +86 -1
  174. package/src/sources/gloomberb-cloud.ts +117 -21
  175. package/src/sources/provider-router.test.ts +122 -0
  176. package/src/sources/provider-router.ts +198 -15
  177. package/src/sources/yahoo-finance.test.ts +44 -0
  178. package/src/sources/yahoo-finance.ts +6 -0
  179. package/src/state/app-bootstrap.ts +20 -7
  180. package/src/state/app-context-selector.test.tsx +189 -4
  181. package/src/state/app-context.test.ts +65 -0
  182. package/src/state/app-context.tsx +48 -16
  183. package/src/state/use-inline-tickers.test.tsx +73 -0
  184. package/src/state/use-inline-tickers.ts +19 -10
  185. package/src/test-support/plugin-runtime.ts +3 -2
  186. package/src/theme/colors.test.ts +75 -0
  187. package/src/theme/colors.ts +76 -6
  188. package/src/theme/theme-context.tsx +26 -0
  189. package/src/theme/themes.test.ts +23 -1
  190. package/src/theme/themes.ts +115 -0
  191. package/src/types/broker.ts +1 -1
  192. package/src/types/capability-route-source.ts +1 -0
  193. package/src/types/data-provider.ts +15 -2
  194. package/src/types/desktop-window.ts +6 -0
  195. package/src/types/financials.ts +2 -0
  196. package/src/types/plugin.ts +1 -3
  197. package/src/ui/host.tsx +7 -0
  198. package/src/utils/api-client.test.ts +32 -0
  199. package/src/utils/api-client.ts +119 -18
  200. package/src/utils/back-navigation.ts +24 -1
  201. package/src/utils/double-escape-close.test.ts +33 -0
  202. package/src/utils/double-escape-close.ts +40 -0
  203. package/src/utils/throttled-fetch.test.ts +22 -0
  204. package/src/utils/throttled-fetch.ts +3 -1
  205. package/src/utils/ticker-search.ts +53 -9
  206. package/src/version.ts +1 -1
  207. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  208. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -1,5 +1,5 @@
1
1
  import { Box, Input, ScrollBox, Text, Textarea, useUiCapabilities } from "../../ui";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import type { RefObject } from "react";
4
4
  import { TextAttributes, type InputRenderable, type ScrollBoxRenderable, type TextareaRenderable } from "../../ui";
5
5
  import { useNativeRenderer } from "../../ui";
@@ -8,7 +8,8 @@ import { Button, NumberField, Spinner, TextField } from "../ui";
8
8
  import { NativeSelect, openNativeSelect, type NativeSelectElement } from "../ui/native-select";
9
9
  import { ToggleList } from "../toggle-list";
10
10
  import {
11
- colors,
11
+ applyTheme,
12
+ clearTransientThemePreview,
12
13
  commandBarBg,
13
14
  commandBarHeadingText,
14
15
  commandBarHoverBg,
@@ -18,9 +19,10 @@ import {
18
19
  commandBarSelectedText,
19
20
  commandBarSubtleText,
20
21
  commandBarText,
22
+ getCurrentThemeId,
23
+ previewTheme,
21
24
  } from "../../theme/colors";
22
25
  import {
23
- getEffectiveThemeId,
24
26
  getFocusedCollectionId,
25
27
  useAppDispatch,
26
28
  useAppSelector,
@@ -28,8 +30,9 @@ import {
28
30
  type AppState,
29
31
  } from "../../state/app-context";
30
32
  import { fuzzyFilter } from "../../utils/fuzzy-search";
31
- import { commands, getThemeOptions, matchPrefix, type Command } from "./command-registry";
32
- import { applyTheme, getCurrentThemeId } from "../../theme/colors";
33
+ import { commands, matchPrefix, type Command } from "./command-registry";
34
+ import { useThemeColors } from "../../theme/theme-context";
35
+ import { ThemePicker, type ThemePickerHandle } from "./theme-picker";
33
36
  import { exportConfig, importConfig, resetAllData, saveConfig } from "../../data/config-store";
34
37
  import type { DataProvider } from "../../types/data-provider";
35
38
  import type { TickerRepository } from "../../data/ticker-repository";
@@ -71,12 +74,20 @@ import {
71
74
  import { notifyGridlockComplete } from "../../plugins/gridlock-notification";
72
75
  import { CHART_RENDERER_PREFERENCES } from "../chart/chart-types";
73
76
  import {
74
- buildSections,
75
77
  getEmptyState,
76
78
  getRowPresentation,
77
79
  resolveCommandBarMode,
78
80
  truncateText,
79
81
  } from "./view-model";
82
+ import {
83
+ buildListRows,
84
+ buildNativeListRows,
85
+ orderListResults,
86
+ type CommandBarListRow,
87
+ type ListScreenState,
88
+ type ResultItem,
89
+ } from "./list-model";
90
+ import { CommandBarConfirmBody } from "./confirm-body";
80
91
  import {
81
92
  createLocalTickerSearchCandidates,
82
93
  normalizeTickerInput,
@@ -156,55 +167,23 @@ interface CommandBarProps {
156
167
  onCheckForUpdates?: () => void | Promise<void>;
157
168
  }
158
169
 
159
- interface ResultItem {
160
- id: string;
161
- label: string;
162
- detail: string;
163
- category: string;
164
- kind: "command" | "ticker" | "search" | "theme" | "plugin" | "action" | "info";
165
- right?: string;
166
- searchText?: string;
167
- themeId?: string;
168
- pluginToggle?: () => void | Promise<void>;
169
- secondaryAction?: () => void | Promise<void>;
170
- checked?: boolean;
171
- current?: boolean;
172
- disabled?: boolean;
173
- action: () => void | Promise<void>;
174
- }
175
-
176
- type ListScreenKind = "root" | "mode" | "picker" | "pane-settings";
177
-
178
- interface ListScreenState {
179
- kind: ListScreenKind;
180
- title: string;
181
- subtitle?: string;
182
- query: string;
183
- selectedIdx: number;
184
- hoveredIdx: number | null;
185
- results: ResultItem[];
186
- searching: boolean;
187
- emptyLabel: string;
188
- emptyDetail: string;
189
- footerLeft: string;
190
- footerRight: string;
191
- }
192
-
193
170
  type WorkflowStringValues = Record<string, string>;
194
171
 
195
- type CommandBarListRow =
196
- | { kind: "spacer"; id: string }
197
- | { kind: "heading"; id: string; label: string }
198
- | { kind: "item"; item: ResultItem; globalIdx: number }
199
- | { kind: "message"; id: string; label: string; dim?: boolean }
200
- | { kind: "spinner"; id: string; label: string }
201
- | { kind: "filler"; id: string };
172
+ const LAYOUT_MOVE_ACTIONS = [
173
+ { id: "layout-move-left", label: "Move Left", direction: "left", unavailableDetail: "No column to the left" },
174
+ { id: "layout-move-right", label: "Move Right", direction: "right", unavailableDetail: "No column to the right" },
175
+ { id: "layout-move-up", label: "Move Up", direction: "above", unavailableDetail: "No pane above" },
176
+ { id: "layout-move-down", label: "Move Down", direction: "below", unavailableDetail: "No pane below" },
177
+ ] as const;
178
+
179
+ type LayoutMoveDirection = (typeof LAYOUT_MOVE_ACTIONS)[number]["direction"];
202
180
 
203
181
  const commandBarLog = debugLog.createLogger("command-bar");
204
182
  const COMMAND_BAR_OVERLAY_Z_INDEX = 2_147_483_646;
205
183
  const COMMAND_BAR_PANEL_Z_INDEX = 2_147_483_647;
206
184
  const NATIVE_COMMAND_BAR_PADDING_X_PX = 14;
207
185
  const NATIVE_COMMAND_BAR_PADDING_Y_PX = 14;
186
+ const QUICK_LOOK_TICKER_SEARCH_OPTIONS = { includeOptionContracts: false } as const;
208
187
 
209
188
  function getDefaultConfigBackupPath(): string {
210
189
  const home = typeof process !== "undefined" ? process.env.HOME : undefined;
@@ -286,40 +265,6 @@ function estimateWorkflowBodyRows(route: CommandBarWorkflowRoute): number {
286
265
  return introRows + fieldRows + statusRows + 1;
287
266
  }
288
267
 
289
- function orderListResults(results: ResultItem[]): ResultItem[] {
290
- return buildSections(results).flatMap((section) => section.items);
291
- }
292
-
293
- function buildListRows(listState: ListScreenState): CommandBarListRow[] {
294
- const rows: CommandBarListRow[] = [];
295
- const sections = buildSections(listState.results);
296
- let globalIdx = 0;
297
- sections.forEach((section, sectionIndex) => {
298
- if (sectionIndex > 0) {
299
- rows.push({ kind: "spacer", id: `spacer:${sectionIndex}:${section.category}` });
300
- }
301
- rows.push({ kind: "heading", id: `heading:${sectionIndex}:${section.category}`, label: section.category });
302
- for (const item of section.items) {
303
- rows.push({ kind: "item", item, globalIdx });
304
- globalIdx += 1;
305
- }
306
- });
307
- return rows;
308
- }
309
-
310
- function buildNativeListRows(listState: ListScreenState, rows: CommandBarListRow[]): CommandBarListRow[] {
311
- if (listState.searching && rows.length === 0) {
312
- return [{ kind: "spinner", id: "searching", label: "Searching…" }];
313
- }
314
- if (rows.length === 0) {
315
- return [{ kind: "message", id: "empty", label: listState.emptyLabel }];
316
- }
317
- if (listState.searching) {
318
- return [...rows, { kind: "spinner", id: "searching", label: "Searching…" }];
319
- }
320
- return rows;
321
- }
322
-
323
268
  function clampListIndex(index: number, length: number): number {
324
269
  return Math.max(0, Math.min(index, Math.max(0, length - 1)));
325
270
  }
@@ -356,6 +301,309 @@ function getVisibleMultiSelectPickerOptions(
356
301
  });
357
302
  }
358
303
 
304
+ type CommandBarListScrollEvent = {
305
+ stopPropagation: () => void;
306
+ preventDefault: () => void;
307
+ scroll?: { direction?: string; delta?: number };
308
+ };
309
+
310
+ interface CommandBarListHeaderProps {
311
+ kind: ListScreenState["kind"];
312
+ query: string;
313
+ queryDisplayWidth: number;
314
+ nativePaneChrome: boolean;
315
+ inputBg: string;
316
+ paletteBg: string;
317
+ paletteText: string;
318
+ paletteSubtleText: string;
319
+ cursorColor: string;
320
+ contentPadding: number;
321
+ rootGhostSuffix: string | null;
322
+ rootQueryLength: number;
323
+ rootShortcutFeedback: string | null;
324
+ onQueryChange: (query: string) => void;
325
+ }
326
+
327
+ const CommandBarListHeader = memo(function CommandBarListHeader({
328
+ kind,
329
+ query,
330
+ queryDisplayWidth,
331
+ nativePaneChrome,
332
+ inputBg,
333
+ paletteBg,
334
+ paletteText,
335
+ paletteSubtleText,
336
+ cursorColor,
337
+ contentPadding,
338
+ rootGhostSuffix,
339
+ rootQueryLength,
340
+ rootShortcutFeedback,
341
+ onQueryChange,
342
+ }: CommandBarListHeaderProps) {
343
+ return (
344
+ <>
345
+ <Box height={1} paddingX={contentPadding}>
346
+ <Box
347
+ width={queryDisplayWidth}
348
+ height={1}
349
+ position="relative"
350
+ backgroundColor={nativePaneChrome ? undefined : inputBg}
351
+ style={nativePaneChrome ? undefined : {
352
+ overflow: "hidden",
353
+ }}
354
+ >
355
+ <Input
356
+ value={query}
357
+ onInput={onQueryChange}
358
+ onChange={onQueryChange}
359
+ placeholder={kind === "root" ? "Search" : "Filter"}
360
+ focused
361
+ width={nativePaneChrome ? "100%" : queryDisplayWidth}
362
+ backgroundColor={nativePaneChrome ? "transparent" : paletteBg}
363
+ focusedBackgroundColor={nativePaneChrome ? "transparent" : paletteBg}
364
+ textColor={paletteText}
365
+ focusedTextColor={paletteText}
366
+ placeholderColor={paletteSubtleText}
367
+ cursorColor={cursorColor}
368
+ />
369
+ {kind === "root" && rootGhostSuffix && (
370
+ <Box
371
+ position="absolute"
372
+ top={0}
373
+ left={Math.max(0, Math.min(rootQueryLength, queryDisplayWidth - 1))}
374
+ width={Math.max(0, queryDisplayWidth - Math.min(rootQueryLength, queryDisplayWidth - 1))}
375
+ height={1}
376
+ >
377
+ <Text fg={paletteSubtleText}>
378
+ {truncateText(
379
+ rootGhostSuffix,
380
+ Math.max(0, queryDisplayWidth - Math.min(rootQueryLength, queryDisplayWidth - 1)),
381
+ )}
382
+ </Text>
383
+ </Box>
384
+ )}
385
+ </Box>
386
+ </Box>
387
+ <Box height={1} paddingX={contentPadding}>
388
+ {kind === "root" && rootShortcutFeedback
389
+ ? (
390
+ <Text fg={paletteSubtleText}>
391
+ {truncateText(rootShortcutFeedback, queryDisplayWidth)}
392
+ </Text>
393
+ )
394
+ : null}
395
+ </Box>
396
+ </>
397
+ );
398
+ });
399
+
400
+ interface CommandBarListItemRowProps {
401
+ item: ResultItem;
402
+ globalIdx: number;
403
+ isSelected: boolean;
404
+ isHovered: boolean;
405
+ contentPadding: number;
406
+ labelWidth: number;
407
+ trailingWidth: number;
408
+ nativePaneChrome: boolean;
409
+ paletteBg: string;
410
+ paletteHoverBg: string;
411
+ paletteSelectedBg: string;
412
+ paletteSelectedText: string;
413
+ paletteSubtleText: string;
414
+ paletteText: string;
415
+ panelBg: string;
416
+ onHoverIndex: (index: number | null) => void;
417
+ onListScroll: (event: CommandBarListScrollEvent) => void;
418
+ onRowMouseDown: (event: any, item: ResultItem, globalIdx: number) => void;
419
+ }
420
+
421
+ const CommandBarListItemRow = memo(function CommandBarListItemRow({
422
+ item,
423
+ globalIdx,
424
+ isSelected,
425
+ isHovered,
426
+ contentPadding,
427
+ labelWidth,
428
+ trailingWidth,
429
+ nativePaneChrome,
430
+ paletteBg,
431
+ paletteHoverBg,
432
+ paletteSelectedBg,
433
+ paletteSelectedText,
434
+ paletteSubtleText,
435
+ paletteText,
436
+ panelBg,
437
+ onHoverIndex,
438
+ onListScroll,
439
+ onRowMouseDown,
440
+ }: CommandBarListItemRowProps) {
441
+ const presentation = getRowPresentation(item, isSelected, trailingWidth > 0);
442
+ const label = truncateText(presentation.label, labelWidth);
443
+ const trailing = truncateText(presentation.trailing, trailingWidth);
444
+
445
+ return (
446
+ <Box
447
+ key={item.id}
448
+ flexDirection="row"
449
+ height={1}
450
+ paddingX={contentPadding}
451
+ backgroundColor={isSelected
452
+ ? paletteSelectedBg
453
+ : isHovered
454
+ ? paletteHoverBg
455
+ : (nativePaneChrome ? panelBg : paletteBg)}
456
+ onMouseMove={() => onHoverIndex(globalIdx)}
457
+ onMouseOut={() => onHoverIndex(null)}
458
+ {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}
459
+ onMouseDown={(event: any) => onRowMouseDown(event, item, globalIdx)}
460
+ data-command-bar-row-selected={nativePaneChrome && isSelected ? "true" : undefined}
461
+ style={nativePaneChrome ? { borderRadius: 6 } : undefined}
462
+ >
463
+ <Box width={labelWidth}>
464
+ <Text fg={isSelected ? paletteSelectedText : presentation.primaryMuted ? paletteSubtleText : paletteText}>
465
+ {label}
466
+ </Text>
467
+ </Box>
468
+ <Box width={trailingWidth}>
469
+ <Text fg={isSelected ? paletteSelectedText : paletteSubtleText}>{trailing}</Text>
470
+ </Box>
471
+ </Box>
472
+ );
473
+ });
474
+
475
+ interface CommandBarListBodyProps {
476
+ visibleListState: ListScreenState;
477
+ nativeListRows: CommandBarListRow[];
478
+ listBodyHeight: number;
479
+ contentPadding: number;
480
+ labelWidth: number;
481
+ nativePaneChrome: boolean;
482
+ nativeListScrollRef: RefObject<ScrollBoxRenderable | null>;
483
+ paletteBg: string;
484
+ paletteHeadingText: string;
485
+ paletteHoverBg: string;
486
+ paletteSelectedBg: string;
487
+ paletteSelectedText: string;
488
+ paletteSubtleText: string;
489
+ paletteText: string;
490
+ panelBg: string;
491
+ queryDisplayWidth: number;
492
+ trailingWidth: number;
493
+ onHoverIndex: (index: number | null) => void;
494
+ onListScroll: (event: CommandBarListScrollEvent) => void;
495
+ onRowMouseDown: (event: any, item: ResultItem, globalIdx: number) => void;
496
+ }
497
+
498
+ const CommandBarListBody = memo(function CommandBarListBody({
499
+ visibleListState,
500
+ nativeListRows,
501
+ listBodyHeight,
502
+ contentPadding,
503
+ labelWidth,
504
+ nativePaneChrome,
505
+ nativeListScrollRef,
506
+ paletteBg,
507
+ paletteHeadingText,
508
+ paletteHoverBg,
509
+ paletteSelectedBg,
510
+ paletteSelectedText,
511
+ paletteSubtleText,
512
+ paletteText,
513
+ panelBg,
514
+ queryDisplayWidth,
515
+ trailingWidth,
516
+ onHoverIndex,
517
+ onListScroll,
518
+ onRowMouseDown,
519
+ }: CommandBarListBodyProps) {
520
+ const visibleRows = useMemo(() => {
521
+ const rows = nativeListRows;
522
+ if (nativePaneChrome) return rows;
523
+ const paddedRows = [...rows];
524
+ while (paddedRows.length < listBodyHeight) {
525
+ paddedRows.push({ kind: "filler", id: `filler:${paddedRows.length}` });
526
+ }
527
+ return paddedRows;
528
+ }, [
529
+ listBodyHeight,
530
+ nativeListRows,
531
+ nativePaneChrome,
532
+ ]);
533
+
534
+ const renderedRows = (
535
+ <>
536
+ {visibleRows.map((row) => {
537
+ if (row.kind === "filler" || row.kind === "spacer") {
538
+ return <Box key={row.id} height={1} />;
539
+ }
540
+ if (row.kind === "spinner") {
541
+ return (
542
+ <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}>
543
+ <Spinner label={row.label} />
544
+ </Box>
545
+ );
546
+ }
547
+ if (row.kind === "message") {
548
+ return (
549
+ <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}>
550
+ <Text fg={paletteText}>{truncateText(row.label, queryDisplayWidth)}</Text>
551
+ </Box>
552
+ );
553
+ }
554
+ if (row.kind === "heading") {
555
+ return (
556
+ <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}>
557
+ <Text attributes={TextAttributes.BOLD} fg={paletteHeadingText}>
558
+ {truncateText(row.label, queryDisplayWidth)}
559
+ </Text>
560
+ </Box>
561
+ );
562
+ }
563
+
564
+ const isSelected = row.globalIdx === visibleListState.selectedIdx;
565
+ const isHovered = row.globalIdx === visibleListState.hoveredIdx && !isSelected;
566
+ return (
567
+ <CommandBarListItemRow
568
+ key={row.item.id}
569
+ item={row.item}
570
+ globalIdx={row.globalIdx}
571
+ isSelected={isSelected}
572
+ isHovered={isHovered}
573
+ contentPadding={contentPadding}
574
+ labelWidth={labelWidth}
575
+ trailingWidth={trailingWidth}
576
+ nativePaneChrome={nativePaneChrome}
577
+ paletteBg={paletteBg}
578
+ paletteHoverBg={paletteHoverBg}
579
+ paletteSelectedBg={paletteSelectedBg}
580
+ paletteSelectedText={paletteSelectedText}
581
+ paletteSubtleText={paletteSubtleText}
582
+ paletteText={paletteText}
583
+ panelBg={panelBg}
584
+ onHoverIndex={onHoverIndex}
585
+ onListScroll={onListScroll}
586
+ onRowMouseDown={onRowMouseDown}
587
+ />
588
+ );
589
+ })}
590
+ </>
591
+ );
592
+
593
+ return (
594
+ <ScrollBox
595
+ ref={nativeListScrollRef}
596
+ flexDirection="column"
597
+ height={listBodyHeight}
598
+ scrollY
599
+ focusable={false}
600
+ {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}
601
+ >
602
+ {renderedRows}
603
+ </ScrollBox>
604
+ );
605
+ });
606
+
359
607
  export function CommandBar({
360
608
  dataProvider,
361
609
  tickerRepository,
@@ -364,6 +612,7 @@ export function CommandBar({
364
612
  onCheckForUpdates,
365
613
  }: CommandBarProps) {
366
614
  const dispatch = useAppDispatch();
615
+ const themeColors = useThemeColors();
367
616
  const config = useAppSelector((state) => state.config);
368
617
  const paneState = useAppSelector((state) => state.paneState);
369
618
  const tickers = useAppSelector((state) => state.tickers);
@@ -425,6 +674,8 @@ export function CommandBar({
425
674
  const rootQueryRef = useRef(rootQuery);
426
675
  rootQueryRef.current = rootQuery;
427
676
  const rootModeInfo = resolveCommandBarMode(rootQuery, availableCommands);
677
+ const rootModeKindRef = useRef(rootModeInfo.kind);
678
+ rootModeKindRef.current = rootModeInfo.kind;
428
679
  const [rootSelectedIdx, setRootSelectedIdx] = useState(0);
429
680
  const [rootHoveredIdx, setRootHoveredIdx] = useState<number | null>(null);
430
681
  const [rootSearching, setRootSearching] = useState(false);
@@ -449,32 +700,50 @@ export function CommandBar({
449
700
  const workflowNativeSelectRefs = useRef(new Map<string, NativeSelectElement>());
450
701
  const workflowScrollRef = useRef<ScrollBoxRenderable | null>(null);
451
702
  const nativeListScrollRef = useRef<ScrollBoxRenderable | null>(null);
703
+ const themePickerRef = useRef<ThemePickerHandle | null>(null);
452
704
  const visibleListStateRef = useRef<ListScreenState | null>(null);
453
705
  const processedLaunchSequenceRef = useRef<number | null>(null);
706
+ const currentThemePreviewRef = useRef<string | null>(null);
454
707
  const currentRoute = routeStack[routeStack.length - 1] ?? null;
455
708
  const currentRouteRef = useRef<CommandBarRoute | null>(currentRoute);
456
709
  currentRouteRef.current = currentRoute;
457
710
  const activeCollectionId = getFocusedCollectionId(state);
458
711
  const activePortfolio = state.config.portfolios.find((portfolio) => portfolio.id === activeCollectionId);
459
712
 
713
+ const applyThemePreview = useCallback((themeId: string | null) => {
714
+ const committedThemeId = stateRef.current.config.theme;
715
+ const preview = themeId && themeId !== committedThemeId ? themeId : null;
716
+ if (preview) {
717
+ previewTheme(preview);
718
+ } else {
719
+ clearTransientThemePreview();
720
+ if (getCurrentThemeId() !== committedThemeId) {
721
+ applyTheme(committedThemeId);
722
+ }
723
+ }
724
+ if (currentThemePreviewRef.current !== preview) {
725
+ currentThemePreviewRef.current = preview;
726
+ dispatch({ type: "PREVIEW_THEME", theme: preview });
727
+ }
728
+ }, [dispatch]);
729
+
460
730
  const clearThemePreview = useCallback((themeId: string | null | undefined) => {
461
- if (!themeId) return;
462
- if (getCurrentThemeId() !== themeId) {
463
- applyTheme(themeId);
731
+ themePickerRef.current?.cancelPreview();
732
+ const targetThemeId = themeId ?? stateRef.current.config.theme;
733
+ if (!targetThemeId) return;
734
+ clearTransientThemePreview();
735
+ if (getCurrentThemeId() !== targetThemeId) {
736
+ applyTheme(targetThemeId);
737
+ }
738
+ if (currentThemePreviewRef.current !== null) {
739
+ currentThemePreviewRef.current = null;
740
+ dispatch({ type: "PREVIEW_THEME", theme: null });
464
741
  }
465
- dispatch({ type: "PREVIEW_THEME", theme: null });
466
742
  }, [dispatch]);
467
743
 
468
744
  const restoreThemePreview = useCallback(() => {
469
- const themeRoute = [...routeStack].reverse().find((route) => (
470
- route.kind === "mode" && route.screen === "themes"
471
- ));
472
- if (themeRoute?.kind === "mode" && themeRoute.themeBaseId) {
473
- clearThemePreview(themeRoute.themeBaseId);
474
- return;
475
- }
476
745
  clearThemePreview(rootThemeBaseIdRef.current);
477
- }, [clearThemePreview, routeStack]);
746
+ }, [clearThemePreview]);
478
747
 
479
748
  const closeAll = useCallback((options?: { revertThemePreview?: boolean }) => {
480
749
  if (options?.revertThemePreview !== false) {
@@ -521,10 +790,6 @@ export function CommandBar({
521
790
  return;
522
791
  }
523
792
 
524
- if (currentRoute.kind === "mode" && currentRoute.screen === "themes" && currentRoute.themeBaseId) {
525
- clearThemePreview(currentRoute.themeBaseId);
526
- }
527
-
528
793
  if (routeStack.length === 1 && currentRoute.restoreMain) {
529
794
  setRootQuery(currentRoute.restoreMain.query);
530
795
  setRootSelectedIdx(currentRoute.restoreMain.selectedIdx);
@@ -584,20 +849,22 @@ export function CommandBar({
584
849
  pluginRegistry.pinTicker(symbol, { floating: true, paneType: "ticker-detail" });
585
850
  }, [pluginRegistry]);
586
851
 
587
- const previewThemeId = useCallback((themeId: string | undefined) => {
588
- if (!themeId) return;
852
+ const commitTheme = useCallback((themeId: string) => {
853
+ themePickerRef.current?.cancelPreview();
854
+ clearTransientThemePreview();
589
855
  if (getCurrentThemeId() !== themeId) {
590
856
  applyTheme(themeId);
591
857
  }
592
- const previewTheme = themeId === stateRef.current.config.theme ? null : themeId;
593
- if (stateRef.current.themePreview !== previewTheme) {
594
- dispatch({ type: "PREVIEW_THEME", theme: previewTheme });
595
- }
858
+ currentThemePreviewRef.current = null;
859
+ dispatch({ type: "SET_THEME", theme: themeId });
596
860
  }, [dispatch]);
597
861
 
598
- const previewThemeSelection = useCallback((listState: ListScreenState, selectedIdx: number) => {
599
- previewThemeId(listState.results[selectedIdx]?.themeId);
600
- }, [previewThemeId]);
862
+ useEffect(() => {
863
+ return () => {
864
+ themePickerRef.current?.cancelPreview();
865
+ clearTransientThemePreview();
866
+ };
867
+ }, []);
601
868
 
602
869
  const focusTicker = useCallback((symbol: string, options?: { forceNewPane?: boolean }) => {
603
870
  const currentState = stateRef.current;
@@ -650,7 +917,7 @@ export function CommandBar({
650
917
  }, [dispatch, persistLayoutChange, pluginRegistry]);
651
918
 
652
919
  const openModeRoute = useCallback((
653
- screen: "ticker-search" | "themes" | "plugins" | "layout",
920
+ screen: "ticker-search" | "plugins" | "layout",
654
921
  initialQuery = "",
655
922
  payload?: Record<string, unknown>,
656
923
  ) => {
@@ -663,7 +930,6 @@ export function CommandBar({
663
930
  query: initialQuery,
664
931
  selectedIdx: 0,
665
932
  hoveredIdx: null,
666
- themeBaseId: screen === "themes" ? stateRef.current.config.theme : undefined,
667
933
  payload,
668
934
  });
669
935
  }, [pushRoute]);
@@ -1220,6 +1486,306 @@ export function CommandBar({
1220
1486
  });
1221
1487
  }, [openConfirmRoute]);
1222
1488
 
1489
+ const buildPluginItems = useCallback((query: string): ResultItem[] => {
1490
+ const disabledPlugins = state.config.disabledPlugins || [];
1491
+ const toggleable = [...pluginRegistry.allPlugins.values()].filter((plugin) => plugin.toggleable);
1492
+ const filtered = query
1493
+ ? toggleable.filter((plugin) => (
1494
+ plugin.name.toLowerCase().includes(query.toLowerCase())
1495
+ || plugin.id.includes(query.toLowerCase())
1496
+ ))
1497
+ : toggleable;
1498
+ return filtered.map((plugin): ResultItem => {
1499
+ const enabled = !disabledPlugins.includes(plugin.id);
1500
+ const toggleAction = () => {
1501
+ dispatch({ type: "TOGGLE_PLUGIN", pluginId: plugin.id });
1502
+ const nextDisabled = enabled
1503
+ ? [...disabledPlugins, plugin.id]
1504
+ : disabledPlugins.filter((entry) => entry !== plugin.id);
1505
+ if (enabled) {
1506
+ for (const paneId of pluginRegistry.getPluginPaneIds(plugin.id)) {
1507
+ pluginRegistry.hidePane(paneId);
1508
+ }
1509
+ }
1510
+ void saveConfig({ ...state.config, disabledPlugins: nextDisabled });
1511
+ };
1512
+ return {
1513
+ id: `plugin:${plugin.id}`,
1514
+ label: plugin.name,
1515
+ detail: plugin.description || "",
1516
+ category: "Plugins",
1517
+ kind: "plugin",
1518
+ checked: enabled,
1519
+ pluginToggle: toggleAction,
1520
+ action: toggleAction,
1521
+ };
1522
+ });
1523
+ }, [dispatch, pluginRegistry, state.config]);
1524
+
1525
+ const buildLayoutItems = useCallback((
1526
+ query: string,
1527
+ options?: { confirmDangerousActions?: boolean },
1528
+ ): ResultItem[] => {
1529
+ const currentLayout = state.config.layout;
1530
+ const focusedPane = state.focusedPaneId ? findPaneInstance(currentLayout, state.focusedPaneId) : null;
1531
+ const focusedPaneDef = focusedPane ? pluginRegistry.panes.get(focusedPane.paneId) : null;
1532
+ const dockedPaneIds = getDockedPaneIds(currentLayout);
1533
+ const layoutHistory = state.layoutHistory[state.config.activeLayoutIndex];
1534
+ const layoutSnapshot = JSON.stringify(currentLayout);
1535
+ const canMove = (direction: LayoutMoveDirection) => (
1536
+ !!focusedPane && JSON.stringify(movePaneRelative(currentLayout, focusedPane.instanceId, direction)) !== layoutSnapshot
1537
+ );
1538
+ const layoutItems: ResultItem[] = [];
1539
+
1540
+ if (focusedPane && focusedPaneDef) {
1541
+ const focusedFloating = currentLayout.floating.find((entry) => entry.instanceId === focusedPane.instanceId);
1542
+ layoutItems.push({
1543
+ id: "layout-toggle-mode",
1544
+ label: focusedFloating ? "Dock Pane" : "Float Pane",
1545
+ detail: focusedFloating ? "Return the focused window to the layout" : "Detach the focused pane into a floating window",
1546
+ category: "Focused Pane",
1547
+ kind: "action",
1548
+ action: () => {
1549
+ const { width, height } = pluginRegistry.getTermSizeFn();
1550
+ const nextLayout = focusedFloating
1551
+ ? dockPane(currentLayout, focusedPane.instanceId)
1552
+ : floatPane(currentLayout, focusedPane.instanceId, width, height, focusedPaneDef);
1553
+ persistLayoutChange(nextLayout);
1554
+ closeAll({ revertThemePreview: false });
1555
+ },
1556
+ });
1557
+
1558
+ LAYOUT_MOVE_ACTIONS.forEach((entry) => {
1559
+ const available = canMove(entry.direction);
1560
+ layoutItems.push({
1561
+ id: entry.id,
1562
+ label: entry.label,
1563
+ detail: available ? "Reposition the focused pane" : entry.unavailableDetail,
1564
+ category: "Focused Pane",
1565
+ kind: available ? "action" : "info",
1566
+ disabled: !available,
1567
+ action: available
1568
+ ? () => {
1569
+ persistLayoutChange(movePaneRelative(currentLayout, focusedPane.instanceId, entry.direction));
1570
+ closeAll({ revertThemePreview: false });
1571
+ }
1572
+ : () => {},
1573
+ });
1574
+ });
1575
+
1576
+ layoutItems.push({
1577
+ id: "layout-swap",
1578
+ label: "Swap With…",
1579
+ detail: dockedPaneIds.length + currentLayout.floating.length > 1
1580
+ ? "Choose another pane to swap positions"
1581
+ : "Need at least two panes",
1582
+ category: "Focused Pane",
1583
+ kind: "action",
1584
+ disabled: dockedPaneIds.length + currentLayout.floating.length <= 1,
1585
+ action: () => {
1586
+ const pickerOptions = [
1587
+ ...dockedPaneIds,
1588
+ ...currentLayout.floating.map((entry) => entry.instanceId),
1589
+ ]
1590
+ .filter((paneId) => paneId !== focusedPane.instanceId)
1591
+ .map((paneId) => {
1592
+ const instance = findPaneInstance(currentLayout, paneId)!;
1593
+ const isFloating = currentLayout.floating.some((entry) => entry.instanceId === paneId);
1594
+ return {
1595
+ id: paneId,
1596
+ label: instance.title || pluginRegistry.panes.get(instance.paneId)?.name || instance.paneId,
1597
+ detail: isFloating ? "Floating window" : "Docked pane",
1598
+ description: isFloating ? "Floating window" : "Docked pane",
1599
+ };
1600
+ });
1601
+ if (pickerOptions.length === 0) return;
1602
+ openPickerRoute({
1603
+ kind: "picker",
1604
+ pickerId: "layout-swap",
1605
+ title: "Swap With…",
1606
+ query: "",
1607
+ selectedIdx: 0,
1608
+ hoveredIdx: null,
1609
+ options: pickerOptions,
1610
+ payload: { sourcePaneId: focusedPane.instanceId },
1611
+ });
1612
+ },
1613
+ });
1614
+
1615
+ layoutItems.push({
1616
+ id: "layout-duplicate",
1617
+ label: "Duplicate Pane",
1618
+ detail: "Create another instance next to the focused pane",
1619
+ category: "Focused Pane",
1620
+ kind: "action",
1621
+ action: () => {
1622
+ duplicatePane(focusedPane.instanceId);
1623
+ closeAll({ revertThemePreview: false });
1624
+ },
1625
+ });
1626
+
1627
+ layoutItems.push({
1628
+ id: "layout-close-pane",
1629
+ label: "Close Pane",
1630
+ detail: "Remove the focused pane from the layout",
1631
+ category: "Focused Pane",
1632
+ kind: "action",
1633
+ action: options?.confirmDangerousActions
1634
+ ? () => {
1635
+ openInlineConfirm({
1636
+ confirmId: "layout-close-pane",
1637
+ title: "Close Pane",
1638
+ body: [`Close "${focusedPane.title || focusedPaneDef.name || focusedPane.instanceId}"?`],
1639
+ confirmLabel: "Close Pane",
1640
+ cancelLabel: "Back",
1641
+ tone: "danger",
1642
+ onConfirm: () => {
1643
+ persistLayoutChange(removePane(currentLayout, focusedPane.instanceId));
1644
+ },
1645
+ });
1646
+ }
1647
+ : () => {
1648
+ persistLayoutChange(removePane(currentLayout, focusedPane.instanceId));
1649
+ closeAll({ revertThemePreview: false });
1650
+ },
1651
+ });
1652
+ } else {
1653
+ layoutItems.push({
1654
+ id: "layout-no-focused-pane",
1655
+ label: "No focused pane",
1656
+ detail: "Focus a pane to show pane-specific layout actions",
1657
+ category: "Focused Pane",
1658
+ kind: "info",
1659
+ action: () => {},
1660
+ });
1661
+ }
1662
+
1663
+ layoutItems.push({
1664
+ id: "layout-undo",
1665
+ label: "Undo Layout Change",
1666
+ detail: (layoutHistory?.past.length ?? 0) > 0 ? "Restore the previous layout state" : "No previous layout state",
1667
+ category: "Current Layout",
1668
+ kind: "action",
1669
+ disabled: (layoutHistory?.past.length ?? 0) === 0,
1670
+ action: () => {
1671
+ if ((layoutHistory?.past.length ?? 0) === 0) return;
1672
+ dispatch({ type: "UNDO_LAYOUT" });
1673
+ closeAll({ revertThemePreview: false });
1674
+ },
1675
+ });
1676
+ layoutItems.push({
1677
+ id: "layout-redo",
1678
+ label: "Redo Layout Change",
1679
+ detail: (layoutHistory?.future.length ?? 0) > 0 ? "Reapply the next layout state" : "No later layout state",
1680
+ category: "Current Layout",
1681
+ kind: "action",
1682
+ disabled: (layoutHistory?.future.length ?? 0) === 0,
1683
+ action: () => {
1684
+ if ((layoutHistory?.future.length ?? 0) === 0) return;
1685
+ dispatch({ type: "REDO_LAYOUT" });
1686
+ closeAll({ revertThemePreview: false });
1687
+ },
1688
+ });
1689
+ layoutItems.push({
1690
+ id: "layout-reset",
1691
+ label: "Reset Current Layout",
1692
+ detail: "Restore the default two-pane layout",
1693
+ category: "Current Layout",
1694
+ kind: "action",
1695
+ action: options?.confirmDangerousActions
1696
+ ? () => {
1697
+ openInlineConfirm({
1698
+ confirmId: "layout-reset",
1699
+ title: "Reset Current Layout",
1700
+ body: ["Reset the current layout to the default two-pane arrangement?"],
1701
+ confirmLabel: "Reset Layout",
1702
+ cancelLabel: "Back",
1703
+ tone: "danger",
1704
+ onConfirm: () => {
1705
+ persistLayoutChange(cloneLayout(DEFAULT_LAYOUT));
1706
+ },
1707
+ });
1708
+ }
1709
+ : () => {
1710
+ persistLayoutChange(cloneLayout(DEFAULT_LAYOUT));
1711
+ closeAll({ revertThemePreview: false });
1712
+ },
1713
+ });
1714
+ layoutItems.push({
1715
+ id: "layout-gridlock",
1716
+ label: "Gridlock All Windows",
1717
+ detail: currentLayout.floating.length > 0
1718
+ ? "Infer a tiled layout from the current window positions"
1719
+ : "Retile all panes from their current arrangement",
1720
+ category: "Current Layout",
1721
+ kind: "action",
1722
+ action: () => {
1723
+ const { width, height } = pluginRegistry.getTermSizeFn();
1724
+ persistLayoutChange(gridlockAllPanes(currentLayout, { x: 0, y: 0, width, height }));
1725
+ notifyGridlockRevert();
1726
+ closeAll({ revertThemePreview: false });
1727
+ },
1728
+ });
1729
+ layoutItems.push({
1730
+ id: "layout-rename",
1731
+ label: "Rename Layout",
1732
+ detail: "Change the current saved layout name",
1733
+ category: "Current Layout",
1734
+ kind: "action",
1735
+ action: () => openBuiltInWorkflow("rename-layout"),
1736
+ });
1737
+ layoutItems.push({
1738
+ id: "layout-duplicate-layout",
1739
+ label: "Duplicate Layout",
1740
+ detail: "Create a copy of the current layout",
1741
+ category: "Current Layout",
1742
+ kind: "action",
1743
+ action: () => {
1744
+ dispatch({ type: "DUPLICATE_LAYOUT", index: state.config.activeLayoutIndex });
1745
+ closeAll({ revertThemePreview: false });
1746
+ },
1747
+ });
1748
+ layoutItems.push({
1749
+ id: "layout-new",
1750
+ label: "New Layout",
1751
+ detail: "Create a fresh saved layout",
1752
+ category: "Current Layout",
1753
+ kind: "action",
1754
+ action: () => openBuiltInWorkflow("new-layout"),
1755
+ });
1756
+
1757
+ state.config.layouts.forEach((savedLayout, index) => {
1758
+ layoutItems.push({
1759
+ id: `layout-switch:${index}`,
1760
+ label: savedLayout.name,
1761
+ detail: index === state.config.activeLayoutIndex ? "Current layout" : "Switch to this saved layout",
1762
+ right: getLayoutPreview(savedLayout.layout),
1763
+ category: "Saved Layouts",
1764
+ kind: "action",
1765
+ current: index === state.config.activeLayoutIndex,
1766
+ action: () => {
1767
+ dispatch({ type: "SWITCH_LAYOUT", index });
1768
+ closeAll({ revertThemePreview: false });
1769
+ },
1770
+ });
1771
+ });
1772
+
1773
+ return query
1774
+ ? fuzzyFilter(layoutItems, query, (item) => `${item.label} ${item.detail} ${item.right || ""}`)
1775
+ : layoutItems;
1776
+ }, [
1777
+ closeAll,
1778
+ dispatch,
1779
+ duplicatePane,
1780
+ notifyGridlockRevert,
1781
+ openBuiltInWorkflow,
1782
+ openInlineConfirm,
1783
+ openPickerRoute,
1784
+ persistLayoutChange,
1785
+ pluginRegistry,
1786
+ state,
1787
+ ]);
1788
+
1223
1789
  const resolvePluginCommandConfirm = useCallback((command: CommandDef) => {
1224
1790
  const context = {
1225
1791
  config: state.config,
@@ -1987,10 +2553,15 @@ export function CommandBar({
1987
2553
  activeCollectionId,
1988
2554
  buildSharedWorkflowDeps(),
1989
2555
  );
1990
- await openPaneTemplateDirect(template, {
2556
+ const createOptions = {
1991
2557
  arg: trimmedList,
1992
2558
  symbols,
1993
- });
2559
+ };
2560
+ if (template.canCreate && !template.canCreate(getPaneTemplateContext(), createOptions)) {
2561
+ openPaneTemplateWorkflow(template, { arg: trimmedArg });
2562
+ return;
2563
+ }
2564
+ await openPaneTemplateDirect(template, createOptions);
1994
2565
  } catch {
1995
2566
  openPaneTemplateWorkflow(template, { arg: trimmedArg });
1996
2567
  }
@@ -2095,20 +2666,24 @@ export function CommandBar({
2095
2666
  tickerSearchCacheRef.current.clear();
2096
2667
  }, [state.tickers]);
2097
2668
 
2669
+ const createQuickLookLocalTickerCandidates = useCallback((tickers: Iterable<TickerRecord>) => (
2670
+ createLocalTickerSearchCandidates(tickers, new Map(), QUICK_LOOK_TICKER_SEARCH_OPTIONS)
2671
+ ), []);
2672
+
2098
2673
  const localTickerSearchResultItems = useCallback((query?: string, options?: {
2099
2674
  category?: string;
2100
2675
  limit?: number;
2101
2676
  }): ResultItem[] => {
2102
2677
  const items = query
2103
- ? rankTickerSearchItems(createLocalTickerSearchCandidates(state.tickers.values()), query)
2104
- : createLocalTickerSearchCandidates(state.tickers.values());
2678
+ ? rankTickerSearchItems(createQuickLookLocalTickerCandidates(state.tickers.values()), query)
2679
+ : createQuickLookLocalTickerCandidates(state.tickers.values());
2105
2680
  return items
2106
2681
  .slice(0, options?.limit)
2107
2682
  .map((candidate) => ({
2108
2683
  ...mapTickerSearchCandidateToResultItem(candidate),
2109
2684
  category: options?.category ?? candidate.category,
2110
2685
  }));
2111
- }, [mapTickerSearchCandidateToResultItem, state.tickers]);
2686
+ }, [createQuickLookLocalTickerCandidates, mapTickerSearchCandidateToResultItem, state.tickers]);
2112
2687
 
2113
2688
  const adaptTickerSearchRouteResult = useCallback((
2114
2689
  item: ResultItem,
@@ -2412,7 +2987,7 @@ export function CommandBar({
2412
2987
  switch (command.id) {
2413
2988
  case "help":
2414
2989
  closeAll({ revertThemePreview: false });
2415
- pluginRegistry.showWidget("help");
2990
+ pluginRegistry.showPane("help");
2416
2991
  return;
2417
2992
  case "pane-settings":
2418
2993
  if (state.focusedPaneId) openPaneSettingsRoute(state.focusedPaneId);
@@ -2528,6 +3103,8 @@ export function CommandBar({
2528
3103
  const importPath = getDefaultConfigBackupPath();
2529
3104
  void importConfig(state.config.dataDir, importPath)
2530
3105
  .then((imported) => {
3106
+ themePickerRef.current?.cancelPreview();
3107
+ clearTransientThemePreview();
2531
3108
  dispatch({ type: "SET_CONFIG", config: imported });
2532
3109
  applyTheme(imported.theme);
2533
3110
  dispatch({ type: "SET_THEME", theme: imported.theme });
@@ -2559,6 +3136,10 @@ export function CommandBar({
2559
3136
  void onCheckForUpdates?.();
2560
3137
  closeAll({ revertThemePreview: false });
2561
3138
  return;
3139
+ case "theme":
3140
+ rootThemeBaseIdRef.current = stateRef.current.config.theme;
3141
+ setRootQuery(arg ? `TH ${arg}` : "TH ");
3142
+ return;
2562
3143
  case "security-description":
2563
3144
  void runSecurityDescriptionShortcut(arg);
2564
3145
  return;
@@ -2595,9 +3176,12 @@ export function CommandBar({
2595
3176
  quitApp,
2596
3177
  runSecurityDescriptionShortcut,
2597
3178
  executeCollectionCommand,
3179
+ setRootQuery,
2598
3180
  ]);
2599
3181
 
2600
3182
  const activeMatch = matchPrefix(rootQuery, availableCommands);
3183
+ const themePickerActive = !currentRoute && activeMatch?.command.id === "theme";
3184
+ const themePickerFilter = themePickerActive ? activeMatch.arg : "";
2601
3185
  const rootSecurityDescriptionArg = activeMatch?.command.id === "security-description" && activeMatch.arg.length >= 1
2602
3186
  ? activeMatch.arg
2603
3187
  : null;
@@ -2619,13 +3203,13 @@ export function CommandBar({
2619
3203
  rootThemeBaseIdRef.current = state.config.theme;
2620
3204
  } else if (rootModeInfo.kind !== "themes" && previousMode === "themes") {
2621
3205
  const rootThemeBaseId = rootThemeBaseIdRef.current;
2622
- if (rootThemeBaseId && getEffectiveThemeId(state) !== rootThemeBaseId) {
3206
+ if (rootThemeBaseId) {
2623
3207
  clearThemePreview(rootThemeBaseId);
2624
3208
  }
2625
3209
  rootThemeBaseIdRef.current = null;
2626
3210
  }
2627
3211
  previousRootModeRef.current = rootModeInfo.kind;
2628
- }, [clearThemePreview, currentRoute, rootModeInfo.kind, state.config.theme, state.themePreview]);
3212
+ }, [clearThemePreview, currentRoute, rootModeInfo.kind, state.config.theme]);
2629
3213
 
2630
3214
  const tickerSearchRouteQuery = currentRoute?.kind === "mode" && currentRoute.screen === "ticker-search"
2631
3215
  ? currentRoute.query
@@ -2673,6 +3257,7 @@ export function CommandBar({
2673
3257
  brokerId: activePortfolio?.brokerId,
2674
3258
  brokerInstanceId: activePortfolio?.brokerInstanceId,
2675
3259
  },
3260
+ ...QUICK_LOOK_TICKER_SEARCH_OPTIONS,
2676
3261
  });
2677
3262
  if (requestId !== searchRequestIdRef.current) return;
2678
3263
  writeTickerSearchCache(
@@ -2959,336 +3544,26 @@ export function CommandBar({
2959
3544
  ) {
2960
3545
  items.push(shortcutItem);
2961
3546
  } else if (match && match.command.id === "plugins") {
2962
- const disabledPlugins = state.config.disabledPlugins || [];
2963
- const toggleable = [...pluginRegistry.allPlugins.values()].filter((plugin) => plugin.toggleable);
2964
- const filtered = match.arg
2965
- ? toggleable.filter((plugin) => (
2966
- plugin.name.toLowerCase().includes(match.arg.toLowerCase())
2967
- || plugin.id.includes(match.arg.toLowerCase())
2968
- ))
2969
- : toggleable;
2970
- for (const plugin of filtered) {
2971
- const enabled = !disabledPlugins.includes(plugin.id);
2972
- const toggleAction = () => {
2973
- dispatch({ type: "TOGGLE_PLUGIN", pluginId: plugin.id });
2974
- const nextDisabled = enabled
2975
- ? [...disabledPlugins, plugin.id]
2976
- : disabledPlugins.filter((entry) => entry !== plugin.id);
2977
- if (enabled) {
2978
- for (const paneId of pluginRegistry.getPluginPaneIds(plugin.id)) {
2979
- pluginRegistry.hideWidget(paneId);
2980
- }
2981
- }
2982
- void saveConfig({ ...state.config, disabledPlugins: nextDisabled });
2983
- };
3547
+ items.push(...buildPluginItems(match.arg));
3548
+ } else if (match && match.command.id === "layout") {
3549
+ items.push(...buildLayoutItems(match.arg, { confirmDangerousActions: true }));
3550
+ } else if (match && match.command.id === "theme") {
3551
+ initialIdx = 0;
3552
+ } else if (match && match.command.id === "security-description") {
3553
+ if (shortcutItem) {
3554
+ items.push(shortcutItem);
3555
+ }
3556
+ if (!match.arg && !shortcutItem) {
2984
3557
  items.push({
2985
- id: `plugin:${plugin.id}`,
2986
- label: plugin.name,
2987
- detail: plugin.description || "",
2988
- category: "Plugins",
2989
- kind: "plugin",
2990
- checked: enabled,
2991
- pluginToggle: toggleAction,
2992
- action: toggleAction,
3558
+ id: "search-hint",
3559
+ label: "Type a ticker symbol",
3560
+ detail: "Open security details after resolving a ticker",
3561
+ category: "Search",
3562
+ kind: "info",
3563
+ action: () => {},
2993
3564
  });
2994
- }
2995
- } else if (match && match.command.id === "layout") {
2996
- const currentLayout = state.config.layout;
2997
- const focusedPane = state.focusedPaneId ? findPaneInstance(currentLayout, state.focusedPaneId) : null;
2998
- const focusedPaneDef = focusedPane ? pluginRegistry.panes.get(focusedPane.paneId) : null;
2999
- const dockedPaneIds = getDockedPaneIds(currentLayout);
3000
- const focusedFloating = focusedPane ? currentLayout.floating.find((entry) => entry.instanceId === focusedPane.instanceId) : null;
3001
- const layoutHistory = state.layoutHistory[state.config.activeLayoutIndex];
3002
- const layoutSnapshot = JSON.stringify(currentLayout);
3003
- const canMove = (direction: "left" | "right" | "above" | "below") => (
3004
- !!focusedPane && JSON.stringify(movePaneRelative(currentLayout, focusedPane.instanceId, direction)) !== layoutSnapshot
3005
- );
3006
- const layoutItems: ResultItem[] = [];
3007
-
3008
- if (focusedPane && focusedPaneDef) {
3009
- layoutItems.push({
3010
- id: "layout-toggle-mode",
3011
- label: focusedFloating ? "Dock Pane" : "Float Pane",
3012
- detail: focusedFloating ? "Return the focused window to the layout" : "Detach the focused pane into a floating window",
3013
- category: "Focused Pane",
3014
- kind: "action",
3015
- action: () => {
3016
- const { width, height } = pluginRegistry.getTermSizeFn();
3017
- const nextLayout = focusedFloating
3018
- ? dockPane(currentLayout, focusedPane.instanceId)
3019
- : floatPane(currentLayout, focusedPane.instanceId, width, height, focusedPaneDef);
3020
- persistLayoutChange(nextLayout);
3021
- closeAll({ revertThemePreview: false });
3022
- },
3023
- });
3024
-
3025
- const moveActions: Array<{
3026
- id: string;
3027
- label: string;
3028
- direction: "left" | "right" | "above" | "below";
3029
- available: boolean;
3030
- unavailableDetail: string;
3031
- }> = [
3032
- { id: "layout-move-left", label: "Move Left", direction: "left", available: canMove("left"), unavailableDetail: "No column to the left" },
3033
- { id: "layout-move-right", label: "Move Right", direction: "right", available: canMove("right"), unavailableDetail: "No column to the right" },
3034
- { id: "layout-move-up", label: "Move Up", direction: "above", available: canMove("above"), unavailableDetail: "No pane above" },
3035
- { id: "layout-move-down", label: "Move Down", direction: "below", available: canMove("below"), unavailableDetail: "No pane below" },
3036
- ];
3037
-
3038
- moveActions.forEach((entry) => {
3039
- layoutItems.push({
3040
- id: entry.id,
3041
- label: entry.label,
3042
- detail: entry.available ? "Reposition the focused pane" : entry.unavailableDetail,
3043
- category: "Focused Pane",
3044
- kind: entry.available ? "action" : "info",
3045
- disabled: !entry.available,
3046
- action: entry.available
3047
- ? () => {
3048
- persistLayoutChange(movePaneRelative(currentLayout, focusedPane.instanceId, entry.direction));
3049
- closeAll({ revertThemePreview: false });
3050
- }
3051
- : () => {},
3052
- });
3053
- });
3054
-
3055
- layoutItems.push({
3056
- id: "layout-swap",
3057
- label: "Swap With…",
3058
- detail: dockedPaneIds.length + currentLayout.floating.length > 1
3059
- ? "Choose another pane to swap positions"
3060
- : "Need at least two panes",
3061
- category: "Focused Pane",
3062
- kind: "action",
3063
- disabled: dockedPaneIds.length + currentLayout.floating.length <= 1,
3064
- action: () => {
3065
- const options = [
3066
- ...dockedPaneIds,
3067
- ...currentLayout.floating.map((entry) => entry.instanceId),
3068
- ]
3069
- .filter((paneId) => paneId !== focusedPane.instanceId)
3070
- .map((paneId) => {
3071
- const instance = findPaneInstance(currentLayout, paneId)!;
3072
- const isFloating = currentLayout.floating.some((entry) => entry.instanceId === paneId);
3073
- return {
3074
- id: paneId,
3075
- label: instance.title || pluginRegistry.panes.get(instance.paneId)?.name || instance.paneId,
3076
- detail: isFloating ? "Floating window" : "Docked pane",
3077
- description: isFloating ? "Floating window" : "Docked pane",
3078
- };
3079
- });
3080
- if (options.length === 0) return;
3081
- openPickerRoute({
3082
- kind: "picker",
3083
- pickerId: "layout-swap",
3084
- title: "Swap With…",
3085
- query: "",
3086
- selectedIdx: 0,
3087
- hoveredIdx: null,
3088
- options,
3089
- payload: { sourcePaneId: focusedPane.instanceId },
3090
- });
3091
- },
3092
- });
3093
-
3094
- layoutItems.push({
3095
- id: "layout-duplicate",
3096
- label: "Duplicate Pane",
3097
- detail: "Create another instance next to the focused pane",
3098
- category: "Focused Pane",
3099
- kind: "action",
3100
- action: () => {
3101
- duplicatePane(focusedPane.instanceId);
3102
- closeAll({ revertThemePreview: false });
3103
- },
3104
- });
3105
-
3106
- layoutItems.push({
3107
- id: "layout-close-pane",
3108
- label: "Close Pane",
3109
- detail: "Remove the focused pane from the layout",
3110
- category: "Focused Pane",
3111
- kind: "action",
3112
- action: () => {
3113
- openInlineConfirm({
3114
- confirmId: "layout-close-pane",
3115
- title: "Close Pane",
3116
- body: [`Close "${focusedPane.title || focusedPaneDef.name || focusedPane.instanceId}"?`],
3117
- confirmLabel: "Close Pane",
3118
- cancelLabel: "Back",
3119
- tone: "danger",
3120
- onConfirm: () => {
3121
- persistLayoutChange(removePane(currentLayout, focusedPane.instanceId));
3122
- },
3123
- });
3124
- },
3125
- });
3126
- } else {
3127
- layoutItems.push({
3128
- id: "layout-no-focused-pane",
3129
- label: "No focused pane",
3130
- detail: "Focus a pane to show pane-specific layout actions",
3131
- category: "Focused Pane",
3132
- kind: "info",
3133
- action: () => {},
3134
- });
3135
- }
3136
-
3137
- layoutItems.push({
3138
- id: "layout-undo",
3139
- label: "Undo Layout Change",
3140
- detail: (layoutHistory?.past.length ?? 0) > 0 ? "Restore the previous layout state" : "No previous layout state",
3141
- category: "Current Layout",
3142
- kind: "action",
3143
- disabled: (layoutHistory?.past.length ?? 0) === 0,
3144
- action: () => {
3145
- if ((layoutHistory?.past.length ?? 0) === 0) return;
3146
- dispatch({ type: "UNDO_LAYOUT" });
3147
- closeAll({ revertThemePreview: false });
3148
- },
3149
- });
3150
- layoutItems.push({
3151
- id: "layout-redo",
3152
- label: "Redo Layout Change",
3153
- detail: (layoutHistory?.future.length ?? 0) > 0 ? "Reapply the next layout state" : "No later layout state",
3154
- category: "Current Layout",
3155
- kind: "action",
3156
- disabled: (layoutHistory?.future.length ?? 0) === 0,
3157
- action: () => {
3158
- if ((layoutHistory?.future.length ?? 0) === 0) return;
3159
- dispatch({ type: "REDO_LAYOUT" });
3160
- closeAll({ revertThemePreview: false });
3161
- },
3162
- });
3163
- layoutItems.push({
3164
- id: "layout-reset",
3165
- label: "Reset Current Layout",
3166
- detail: "Restore the default two-pane layout",
3167
- category: "Current Layout",
3168
- kind: "action",
3169
- action: () => {
3170
- openInlineConfirm({
3171
- confirmId: "layout-reset",
3172
- title: "Reset Current Layout",
3173
- body: ["Reset the current layout to the default two-pane arrangement?"],
3174
- confirmLabel: "Reset Layout",
3175
- cancelLabel: "Back",
3176
- tone: "danger",
3177
- onConfirm: () => {
3178
- persistLayoutChange(cloneLayout(DEFAULT_LAYOUT));
3179
- },
3180
- });
3181
- },
3182
- });
3183
- layoutItems.push({
3184
- id: "layout-gridlock",
3185
- label: "Gridlock All Windows",
3186
- detail: currentLayout.floating.length > 0
3187
- ? "Infer a tiled layout from the current window positions"
3188
- : "Retile all panes from their current arrangement",
3189
- category: "Current Layout",
3190
- kind: "action",
3191
- action: () => {
3192
- const { width, height } = pluginRegistry.getTermSizeFn();
3193
- persistLayoutChange(gridlockAllPanes(currentLayout, { x: 0, y: 0, width, height }));
3194
- notifyGridlockRevert();
3195
- closeAll({ revertThemePreview: false });
3196
- },
3197
- });
3198
- layoutItems.push({
3199
- id: "layout-rename",
3200
- label: "Rename Layout",
3201
- detail: "Change the current saved layout name",
3202
- category: "Current Layout",
3203
- kind: "action",
3204
- action: () => openBuiltInWorkflow("rename-layout"),
3205
- });
3206
- layoutItems.push({
3207
- id: "layout-duplicate-layout",
3208
- label: "Duplicate Layout",
3209
- detail: "Create a copy of the current layout",
3210
- category: "Current Layout",
3211
- kind: "action",
3212
- action: () => {
3213
- dispatch({ type: "DUPLICATE_LAYOUT", index: state.config.activeLayoutIndex });
3214
- closeAll({ revertThemePreview: false });
3215
- },
3216
- });
3217
- layoutItems.push({
3218
- id: "layout-new",
3219
- label: "New Layout",
3220
- detail: "Create a fresh saved layout",
3221
- category: "Current Layout",
3222
- kind: "action",
3223
- action: () => openBuiltInWorkflow("new-layout"),
3224
- });
3225
-
3226
- state.config.layouts.forEach((savedLayout, index) => {
3227
- layoutItems.push({
3228
- id: `layout-switch:${index}`,
3229
- label: savedLayout.name,
3230
- detail: index === state.config.activeLayoutIndex ? "Current layout" : "Switch to this saved layout",
3231
- right: getLayoutPreview(savedLayout.layout),
3232
- category: "Saved Layouts",
3233
- kind: "action",
3234
- current: index === state.config.activeLayoutIndex,
3235
- action: () => {
3236
- dispatch({ type: "SWITCH_LAYOUT", index });
3237
- closeAll({ revertThemePreview: false });
3238
- },
3239
- });
3240
- });
3241
-
3242
- items.push(...(match.arg
3243
- ? fuzzyFilter(layoutItems, match.arg, (item) => `${item.label} ${item.detail} ${item.right || ""}`)
3244
- : layoutItems));
3245
- } else if (match && match.command.id === "theme") {
3246
- const savedThemeId = rootThemeBaseIdRef.current ?? state.config.theme;
3247
- const themeOptions = getThemeOptions();
3248
- const filtered = match.arg
3249
- ? themeOptions.filter((theme) => (
3250
- theme.name.toLowerCase().includes(match.arg.toLowerCase())
3251
- || theme.id.includes(match.arg.toLowerCase())
3252
- ))
3253
- : themeOptions;
3254
- filtered.forEach((theme, index) => {
3255
- const isSaved = theme.id === savedThemeId;
3256
- if (isSaved) initialIdx = index;
3257
- items.push({
3258
- id: `theme:${theme.id}`,
3259
- label: theme.name,
3260
- detail: theme.description,
3261
- category: "Themes",
3262
- kind: "theme",
3263
- current: isSaved,
3264
- themeId: theme.id,
3265
- action: () => {
3266
- rootThemeBaseIdRef.current = theme.id;
3267
- const nextConfig = {
3268
- ...state.config,
3269
- theme: theme.id,
3270
- };
3271
- dispatch({ type: "SET_THEME", theme: theme.id });
3272
- void saveConfig(nextConfig);
3273
- closeAll({ revertThemePreview: false });
3274
- },
3275
- });
3276
- });
3277
- } else if (match && match.command.id === "security-description") {
3278
- if (shortcutItem) {
3279
- items.push(shortcutItem);
3280
- }
3281
- if (!match.arg && !shortcutItem) {
3282
- items.push({
3283
- id: "search-hint",
3284
- label: "Type a ticker symbol",
3285
- detail: "Open security details after resolving a ticker",
3286
- category: "Search",
3287
- kind: "info",
3288
- action: () => {},
3289
- });
3290
- } else if (match.arg) {
3291
- items.push(...localTickerSearchResultItems(match.arg, { limit: 6 }));
3565
+ } else if (match.arg) {
3566
+ items.push(...localTickerSearchResultItems(match.arg, { limit: 6 }));
3292
3567
  }
3293
3568
  } else if (match && isCollectionCommand(match.command.id)) {
3294
3569
  if (shortcutItem) items.push(shortcutItem);
@@ -3300,18 +3575,26 @@ export function CommandBar({
3300
3575
  const recentSymbols = state.recentTickers.slice(0, maxDefaultTickers);
3301
3576
  const recentTickers = recentSymbols
3302
3577
  .map((symbol) => state.tickers.get(symbol))
3303
- .filter((ticker): ticker is NonNullable<typeof ticker> => ticker != null);
3578
+ .filter((ticker): ticker is NonNullable<typeof ticker> => (
3579
+ ticker != null && createQuickLookLocalTickerCandidates([ticker]).length > 0
3580
+ ));
3304
3581
  if (recentTickers.length < maxDefaultTickers) {
3305
3582
  const seen = new Set(recentSymbols);
3306
3583
  for (const ticker of state.tickers.values()) {
3307
3584
  if (recentTickers.length >= maxDefaultTickers) break;
3585
+ if (createQuickLookLocalTickerCandidates([ticker]).length === 0) continue;
3308
3586
  if (!seen.has(ticker.metadata.ticker)) recentTickers.push(ticker);
3309
3587
  }
3310
3588
  }
3311
- items.push(...recentTickers.map((ticker) => ({
3312
- ...mapTickerSearchCandidateToResultItem(createLocalTickerSearchCandidates([ticker])[0]!),
3313
- category: "Tickers",
3314
- })));
3589
+ items.push(...recentTickers.flatMap((ticker) => {
3590
+ const candidate = createQuickLookLocalTickerCandidates([ticker])[0];
3591
+ return candidate
3592
+ ? [{
3593
+ ...mapTickerSearchCandidateToResultItem(candidate),
3594
+ category: "Tickers",
3595
+ }]
3596
+ : [];
3597
+ }));
3315
3598
  items.push(...paneShortcutItems());
3316
3599
  for (const command of availableCommands) {
3317
3600
  const item = commandToItem(command);
@@ -3341,19 +3624,15 @@ export function CommandBar({
3341
3624
  activeTickerData,
3342
3625
  activeTickerSymbol,
3343
3626
  availableCommands,
3344
- closeAll,
3627
+ buildLayoutItems,
3628
+ buildPluginItems,
3629
+ createQuickLookLocalTickerCandidates,
3345
3630
  createPluginCommandItem,
3346
3631
  currentRoute,
3347
- dispatch,
3348
- duplicatePane,
3349
3632
  executeCollectionCommand,
3350
3633
  localTickerSearchResultItems,
3351
3634
  nonShortcutPaneTemplateItems,
3352
- notifyGridlockRevert,
3353
- openBuiltInWorkflow,
3354
- openPickerRoute,
3355
3635
  paneShortcutItems,
3356
- persistLayoutChange,
3357
3636
  pluginCommandItems,
3358
3637
  pluginRegistry,
3359
3638
  rootModeInfo.kind,
@@ -3435,6 +3714,7 @@ export function CommandBar({
3435
3714
  brokerId: activeSearchPortfolio?.brokerId,
3436
3715
  brokerInstanceId: activeSearchPortfolio?.brokerInstanceId,
3437
3716
  },
3717
+ ...QUICK_LOOK_TICKER_SEARCH_OPTIONS,
3438
3718
  });
3439
3719
  if (requestId !== rootSearchRequestIdRef.current) return;
3440
3720
  writeTickerSearchCache(
@@ -3484,6 +3764,7 @@ export function CommandBar({
3484
3764
  }
3485
3765
  return rootResultModel.items;
3486
3766
  }, [rootProviderResults, rootProviderResultsQuery, rootResultModel.items, rootSecurityDescriptionArg]);
3767
+ const orderedRootResults = useMemo(() => orderListResults(rootResults), [rootResults]);
3487
3768
 
3488
3769
  const rootGhostCompletion = !currentRoute && rootShortcutIntent.kind === "inferred-complete"
3489
3770
  ? rootShortcutIntent.completionQuery
@@ -3689,12 +3970,15 @@ export function CommandBar({
3689
3970
 
3690
3971
  if (match.command.id === "theme") {
3691
3972
  return {
3692
- id: "theme-route",
3973
+ id: "theme-picker",
3693
3974
  label: "Change Theme",
3694
3975
  detail: "Preview and apply themes",
3695
3976
  category: "Themes",
3696
3977
  kind: "command",
3697
- action: () => openModeRoute("themes", match.arg),
3978
+ action: () => {
3979
+ rootThemeBaseIdRef.current = stateRef.current.config.theme;
3980
+ setRootQuery(match.arg ? `TH ${match.arg}` : "TH ");
3981
+ },
3698
3982
  };
3699
3983
  }
3700
3984
 
@@ -3760,6 +4044,7 @@ export function CommandBar({
3760
4044
  openModeRoute,
3761
4045
  runDirectCommand,
3762
4046
  runSecurityDescriptionShortcut,
4047
+ setRootQuery,
3763
4048
  ]);
3764
4049
 
3765
4050
  const routeListState = useMemo<ListScreenState | null>(() => {
@@ -3775,7 +4060,7 @@ export function CommandBar({
3775
4060
  query: rootQuery,
3776
4061
  selectedIdx: rootSelectedIdx,
3777
4062
  hoveredIdx: rootHoveredIdx,
3778
- results: orderListResults(rootResults),
4063
+ results: orderedRootResults,
3779
4064
  searching: rootSearching,
3780
4065
  emptyLabel: emptyState.label,
3781
4066
  emptyDetail: emptyState.detail,
@@ -3786,81 +4071,8 @@ export function CommandBar({
3786
4071
 
3787
4072
  if (currentRoute.kind === "mode") {
3788
4073
  switch (currentRoute.screen) {
3789
- case "themes": {
3790
- const themeOptions = getThemeOptions();
3791
- const filtered = currentRoute.query
3792
- ? themeOptions.filter((theme) => (
3793
- theme.name.toLowerCase().includes(currentRoute.query.toLowerCase())
3794
- || theme.id.includes(currentRoute.query.toLowerCase())
3795
- ))
3796
- : themeOptions;
3797
- const results = filtered.map((theme) => ({
3798
- id: `theme:${theme.id}`,
3799
- label: theme.name,
3800
- detail: theme.description,
3801
- category: "Themes",
3802
- kind: "theme" as const,
3803
- current: theme.id === currentRoute.themeBaseId,
3804
- themeId: theme.id,
3805
- action: () => {
3806
- const nextConfig = {
3807
- ...state.config,
3808
- theme: theme.id,
3809
- };
3810
- dispatch({ type: "SET_THEME", theme: theme.id });
3811
- void saveConfig(nextConfig);
3812
- closeAll({ revertThemePreview: false });
3813
- },
3814
- }));
3815
- return {
3816
- kind: "mode",
3817
- title: "Change Theme",
3818
- subtitle: "Preview themes with the keyboard, then save.",
3819
- query: currentRoute.query,
3820
- selectedIdx: currentRoute.selectedIdx,
3821
- hoveredIdx: currentRoute.hoveredIdx,
3822
- results: orderListResults(results),
3823
- searching: false,
3824
- emptyLabel: getEmptyState("themes", currentRoute.query).label,
3825
- emptyDetail: getEmptyState("themes", currentRoute.query).detail,
3826
- footerLeft: getScreenFooterLeft(currentRoute),
3827
- footerRight: getScreenFooterRight(currentRoute),
3828
- };
3829
- }
3830
4074
  case "plugins": {
3831
- const disabledPlugins = state.config.disabledPlugins || [];
3832
- const toggleable = [...pluginRegistry.allPlugins.values()].filter((plugin) => plugin.toggleable);
3833
- const filtered = currentRoute.query
3834
- ? toggleable.filter((plugin) => (
3835
- plugin.name.toLowerCase().includes(currentRoute.query.toLowerCase())
3836
- || plugin.id.includes(currentRoute.query.toLowerCase())
3837
- ))
3838
- : toggleable;
3839
- const results = filtered.map((plugin) => {
3840
- const enabled = !disabledPlugins.includes(plugin.id);
3841
- const toggleAction = () => {
3842
- dispatch({ type: "TOGGLE_PLUGIN", pluginId: plugin.id });
3843
- const nextDisabled = enabled
3844
- ? [...disabledPlugins, plugin.id]
3845
- : disabledPlugins.filter((entry) => entry !== plugin.id);
3846
- if (enabled) {
3847
- for (const paneId of pluginRegistry.getPluginPaneIds(plugin.id)) {
3848
- pluginRegistry.hideWidget(paneId);
3849
- }
3850
- }
3851
- void saveConfig({ ...state.config, disabledPlugins: nextDisabled });
3852
- };
3853
- return {
3854
- id: `plugin:${plugin.id}`,
3855
- label: plugin.name,
3856
- detail: plugin.description || "",
3857
- category: "Plugins",
3858
- kind: "plugin" as const,
3859
- checked: enabled,
3860
- pluginToggle: toggleAction,
3861
- action: toggleAction,
3862
- };
3863
- });
4075
+ const results = buildPluginItems(currentRoute.query);
3864
4076
  return {
3865
4077
  kind: "mode",
3866
4078
  title: "Manage Plugins",
@@ -3877,237 +4089,7 @@ export function CommandBar({
3877
4089
  };
3878
4090
  }
3879
4091
  case "layout": {
3880
- const currentLayout = state.config.layout;
3881
- const focusedPane = state.focusedPaneId ? findPaneInstance(currentLayout, state.focusedPaneId) : null;
3882
- const focusedPaneDef = focusedPane ? pluginRegistry.panes.get(focusedPane.paneId) : null;
3883
- const dockedPaneIds = getDockedPaneIds(currentLayout);
3884
- const layoutHistory = state.layoutHistory[state.config.activeLayoutIndex];
3885
- const layoutSnapshot = JSON.stringify(currentLayout);
3886
- const canMove = (direction: "left" | "right" | "above" | "below") => (
3887
- !!focusedPane && JSON.stringify(movePaneRelative(currentLayout, focusedPane.instanceId, direction)) !== layoutSnapshot
3888
- );
3889
-
3890
- const layoutItems: ResultItem[] = [];
3891
- if (focusedPane && focusedPaneDef) {
3892
- const focusedFloating = currentLayout.floating.find((entry) => entry.instanceId === focusedPane.instanceId);
3893
- layoutItems.push({
3894
- id: "layout-toggle-mode",
3895
- label: focusedFloating ? "Dock Pane" : "Float Pane",
3896
- detail: focusedFloating ? "Return the focused window to the layout" : "Detach the focused pane into a floating window",
3897
- category: "Focused Pane",
3898
- kind: "action",
3899
- action: () => {
3900
- const { width, height } = pluginRegistry.getTermSizeFn();
3901
- const nextLayout = focusedFloating
3902
- ? dockPane(currentLayout, focusedPane.instanceId)
3903
- : floatPane(currentLayout, focusedPane.instanceId, width, height, focusedPaneDef);
3904
- persistLayoutChange(nextLayout);
3905
- closeAll({ revertThemePreview: false });
3906
- },
3907
- });
3908
-
3909
- const moveActions: Array<{
3910
- id: string;
3911
- label: string;
3912
- direction: "left" | "right" | "above" | "below";
3913
- available: boolean;
3914
- unavailableDetail: string;
3915
- }> = [
3916
- { id: "layout-move-left", label: "Move Left", direction: "left", available: canMove("left"), unavailableDetail: "No column to the left" },
3917
- { id: "layout-move-right", label: "Move Right", direction: "right", available: canMove("right"), unavailableDetail: "No column to the right" },
3918
- { id: "layout-move-up", label: "Move Up", direction: "above", available: canMove("above"), unavailableDetail: "No pane above" },
3919
- { id: "layout-move-down", label: "Move Down", direction: "below", available: canMove("below"), unavailableDetail: "No pane below" },
3920
- ];
3921
-
3922
- moveActions.forEach((entry) => {
3923
- layoutItems.push({
3924
- id: entry.id,
3925
- label: entry.label,
3926
- detail: entry.available ? "Reposition the focused pane" : entry.unavailableDetail,
3927
- category: "Focused Pane",
3928
- kind: entry.available ? "action" : "info",
3929
- disabled: !entry.available,
3930
- action: entry.available
3931
- ? () => {
3932
- persistLayoutChange(movePaneRelative(currentLayout, focusedPane.instanceId, entry.direction));
3933
- closeAll({ revertThemePreview: false });
3934
- }
3935
- : () => {},
3936
- });
3937
- });
3938
-
3939
- layoutItems.push({
3940
- id: "layout-swap",
3941
- label: "Swap With…",
3942
- detail: dockedPaneIds.length + currentLayout.floating.length > 1
3943
- ? "Choose another pane to swap positions"
3944
- : "Need at least two panes",
3945
- category: "Focused Pane",
3946
- kind: "action",
3947
- disabled: dockedPaneIds.length + currentLayout.floating.length <= 1,
3948
- action: () => {
3949
- const options = [
3950
- ...dockedPaneIds,
3951
- ...currentLayout.floating.map((entry) => entry.instanceId),
3952
- ]
3953
- .filter((paneId) => paneId !== focusedPane.instanceId)
3954
- .map((paneId) => {
3955
- const instance = findPaneInstance(currentLayout, paneId)!;
3956
- return {
3957
- id: paneId,
3958
- label: instance.title || pluginRegistry.panes.get(instance.paneId)?.name || instance.paneId,
3959
- detail: currentLayout.floating.some((entry) => entry.instanceId === paneId) ? "Floating window" : "Docked pane",
3960
- description: currentLayout.floating.some((entry) => entry.instanceId === paneId) ? "Floating window" : "Docked pane",
3961
- };
3962
- });
3963
- if (options.length === 0) return;
3964
- openPickerRoute({
3965
- kind: "picker",
3966
- pickerId: "layout-swap",
3967
- title: "Swap With…",
3968
- query: "",
3969
- selectedIdx: 0,
3970
- hoveredIdx: null,
3971
- options,
3972
- payload: { sourcePaneId: focusedPane.instanceId },
3973
- });
3974
- },
3975
- });
3976
-
3977
- layoutItems.push({
3978
- id: "layout-duplicate",
3979
- label: "Duplicate Pane",
3980
- detail: "Create another instance next to the focused pane",
3981
- category: "Focused Pane",
3982
- kind: "action",
3983
- action: () => {
3984
- duplicatePane(focusedPane.instanceId);
3985
- closeAll({ revertThemePreview: false });
3986
- },
3987
- });
3988
-
3989
- layoutItems.push({
3990
- id: "layout-close-pane",
3991
- label: "Close Pane",
3992
- detail: "Remove the focused pane from the layout",
3993
- category: "Focused Pane",
3994
- kind: "action",
3995
- action: () => {
3996
- persistLayoutChange(removePane(currentLayout, focusedPane.instanceId));
3997
- closeAll({ revertThemePreview: false });
3998
- },
3999
- });
4000
- } else {
4001
- layoutItems.push({
4002
- id: "layout-no-focused-pane",
4003
- label: "No focused pane",
4004
- detail: "Focus a pane to show pane-specific layout actions",
4005
- category: "Focused Pane",
4006
- kind: "info",
4007
- action: () => {},
4008
- });
4009
- }
4010
-
4011
- layoutItems.push({
4012
- id: "layout-undo",
4013
- label: "Undo Layout Change",
4014
- detail: (layoutHistory?.past.length ?? 0) > 0 ? "Restore the previous layout state" : "No previous layout state",
4015
- category: "Current Layout",
4016
- kind: "action",
4017
- disabled: (layoutHistory?.past.length ?? 0) === 0,
4018
- action: () => {
4019
- if ((layoutHistory?.past.length ?? 0) === 0) return;
4020
- dispatch({ type: "UNDO_LAYOUT" });
4021
- closeAll({ revertThemePreview: false });
4022
- },
4023
- });
4024
- layoutItems.push({
4025
- id: "layout-redo",
4026
- label: "Redo Layout Change",
4027
- detail: (layoutHistory?.future.length ?? 0) > 0 ? "Reapply the next layout state" : "No later layout state",
4028
- category: "Current Layout",
4029
- kind: "action",
4030
- disabled: (layoutHistory?.future.length ?? 0) === 0,
4031
- action: () => {
4032
- if ((layoutHistory?.future.length ?? 0) === 0) return;
4033
- dispatch({ type: "REDO_LAYOUT" });
4034
- closeAll({ revertThemePreview: false });
4035
- },
4036
- });
4037
- layoutItems.push({
4038
- id: "layout-reset",
4039
- label: "Reset Current Layout",
4040
- detail: "Restore the default two-pane layout",
4041
- category: "Current Layout",
4042
- kind: "action",
4043
- action: () => {
4044
- persistLayoutChange(cloneLayout(DEFAULT_LAYOUT));
4045
- closeAll({ revertThemePreview: false });
4046
- },
4047
- });
4048
- layoutItems.push({
4049
- id: "layout-gridlock",
4050
- label: "Gridlock All Windows",
4051
- detail: currentLayout.floating.length > 0
4052
- ? "Infer a tiled layout from the current window positions"
4053
- : "Retile all panes from their current arrangement",
4054
- category: "Current Layout",
4055
- kind: "action",
4056
- action: () => {
4057
- const { width, height } = pluginRegistry.getTermSizeFn();
4058
- persistLayoutChange(gridlockAllPanes(currentLayout, { x: 0, y: 0, width, height }));
4059
- notifyGridlockRevert();
4060
- closeAll({ revertThemePreview: false });
4061
- },
4062
- });
4063
- layoutItems.push({
4064
- id: "layout-rename",
4065
- label: "Rename Layout",
4066
- detail: "Change the current saved layout name",
4067
- category: "Current Layout",
4068
- kind: "action",
4069
- action: () => openBuiltInWorkflow("rename-layout"),
4070
- });
4071
- layoutItems.push({
4072
- id: "layout-duplicate-layout",
4073
- label: "Duplicate Layout",
4074
- detail: "Create a copy of the current layout",
4075
- category: "Current Layout",
4076
- kind: "action",
4077
- action: () => {
4078
- dispatch({ type: "DUPLICATE_LAYOUT", index: state.config.activeLayoutIndex });
4079
- closeAll({ revertThemePreview: false });
4080
- },
4081
- });
4082
- layoutItems.push({
4083
- id: "layout-new",
4084
- label: "New Layout",
4085
- detail: "Create a fresh saved layout",
4086
- category: "Current Layout",
4087
- kind: "action",
4088
- action: () => openBuiltInWorkflow("new-layout"),
4089
- });
4090
-
4091
- state.config.layouts.forEach((savedLayout, index) => {
4092
- layoutItems.push({
4093
- id: `layout-switch:${index}`,
4094
- label: savedLayout.name,
4095
- detail: index === state.config.activeLayoutIndex ? "Current layout" : "Switch to this saved layout",
4096
- right: getLayoutPreview(savedLayout.layout),
4097
- category: "Saved Layouts",
4098
- kind: "action",
4099
- current: index === state.config.activeLayoutIndex,
4100
- action: () => {
4101
- dispatch({ type: "SWITCH_LAYOUT", index });
4102
- closeAll({ revertThemePreview: false });
4103
- },
4104
- });
4105
- });
4106
-
4107
- const results = currentRoute.query
4108
- ? fuzzyFilter(layoutItems, currentRoute.query, (item) => `${item.label} ${item.detail} ${item.right || ""}`)
4109
- : layoutItems;
4110
-
4092
+ const results = buildLayoutItems(currentRoute.query);
4111
4093
  return {
4112
4094
  kind: "mode",
4113
4095
  title: "Layout Actions",
@@ -4218,6 +4200,8 @@ export function CommandBar({
4218
4200
  activeMatch,
4219
4201
  activeTickerData,
4220
4202
  activeTickerSymbol,
4203
+ buildLayoutItems,
4204
+ buildPluginItems,
4221
4205
  closeAll,
4222
4206
  createPaneTemplateItem,
4223
4207
  currentRoute,
@@ -4241,7 +4225,7 @@ export function CommandBar({
4241
4225
  rootHoveredIdx,
4242
4226
  rootModeInfo.kind,
4243
4227
  rootQuery,
4244
- rootResults,
4228
+ orderedRootResults,
4245
4229
  rootSearching,
4246
4230
  rootSelectedIdx,
4247
4231
  rootShortcutIntent,
@@ -4278,8 +4262,9 @@ export function CommandBar({
4278
4262
  }, [currentRoute, routeListState, updateTopRoute]);
4279
4263
 
4280
4264
  const setActiveListQuery = useCallback((nextQuery: string) => {
4281
- if (!currentRoute) {
4282
- if (rootModeInfo.kind === "themes" && resolveCommandBarMode(nextQuery, availableCommands).kind !== "themes") {
4265
+ const route = currentRouteRef.current;
4266
+ if (!route) {
4267
+ if (rootModeKindRef.current === "themes" && resolveCommandBarMode(nextQuery, availableCommands).kind !== "themes") {
4283
4268
  clearThemePreview(rootThemeBaseIdRef.current ?? stateRef.current.config.theme);
4284
4269
  rootThemeBaseIdRef.current = null;
4285
4270
  }
@@ -4287,7 +4272,7 @@ export function CommandBar({
4287
4272
  return;
4288
4273
  }
4289
4274
 
4290
- if (currentRoute.kind === "mode" || currentRoute.kind === "picker" || currentRoute.kind === "pane-settings") {
4275
+ if (route.kind === "mode" || route.kind === "picker" || route.kind === "pane-settings") {
4291
4276
  updateTopRoute((route) => {
4292
4277
  if (route.kind === "mode" || route.kind === "picker" || route.kind === "pane-settings") {
4293
4278
  return { ...route, query: nextQuery, selectedIdx: 0, hoveredIdx: null };
@@ -4295,19 +4280,21 @@ export function CommandBar({
4295
4280
  return route;
4296
4281
  });
4297
4282
  }
4298
- }, [availableCommands, clearThemePreview, currentRoute, rootModeInfo.kind, setRootQuery, updateTopRoute]);
4283
+ }, [availableCommands, clearThemePreview, setRootQuery, updateTopRoute]);
4299
4284
 
4300
- const moveListSelection = useCallback((delta: number) => {
4285
+ const applyListSelectionDelta = useCallback((delta: number) => {
4301
4286
  const listState = visibleListStateRef.current;
4302
4287
  if (!listState || listState.results.length === 0 || delta === 0) return;
4303
4288
  const nextIndex = clampListIndex(listState.selectedIdx + delta, listState.results.length);
4289
+ const selectionChanged = nextIndex !== listState.selectedIdx;
4290
+ const hoverChanged = listState.hoveredIdx !== null;
4291
+ if (!selectionChanged && !hoverChanged) return;
4304
4292
  const nextListState = { ...listState, selectedIdx: nextIndex, hoveredIdx: null };
4305
4293
  visibleListStateRef.current = nextListState;
4306
- previewThemeSelection(nextListState, nextIndex);
4307
4294
 
4308
4295
  if (!currentRouteRef.current) {
4309
- setRootSelectedIdx(nextIndex);
4310
- setRootHoveredIdx(null);
4296
+ setRootSelectedIdx((current) => (current === nextIndex ? current : nextIndex));
4297
+ setRootHoveredIdx((current) => (current === null ? current : null));
4311
4298
  return;
4312
4299
  }
4313
4300
  setRouteStack((current) => {
@@ -4317,24 +4304,34 @@ export function CommandBar({
4317
4304
  if (!top || (top.kind !== "mode" && top.kind !== "picker" && top.kind !== "pane-settings")) {
4318
4305
  return current;
4319
4306
  }
4307
+ if (top.selectedIdx === nextIndex && top.hoveredIdx === null) return current;
4320
4308
  next[next.length - 1] = { ...top, selectedIdx: nextIndex, hoveredIdx: null };
4321
4309
  return next;
4322
4310
  });
4323
- }, [previewThemeSelection]);
4311
+ }, []);
4312
+
4313
+ const moveListSelection = useCallback((delta: number) => {
4314
+ applyListSelectionDelta(delta);
4315
+ }, [applyListSelectionDelta]);
4324
4316
 
4325
4317
  const setHoveredIndex = useCallback((index: number | null) => {
4326
- if (!currentRoute) {
4318
+ if (!currentRouteRef.current) {
4327
4319
  setRootHoveredIdx((current) => (current === index ? current : index));
4328
4320
  return;
4329
4321
  }
4330
- updateTopRoute((route) => {
4322
+ setRouteStack((current) => {
4323
+ if (current.length === 0) return current;
4324
+ const next = [...current];
4325
+ const route = next[next.length - 1];
4326
+ if (!route) return current;
4331
4327
  if (route.kind === "mode" || route.kind === "picker" || route.kind === "pane-settings") {
4332
- if (route.hoveredIdx === index) return route;
4333
- return { ...route, hoveredIdx: index };
4328
+ if (route.hoveredIdx === index) return current;
4329
+ next[next.length - 1] = { ...route, hoveredIdx: index };
4330
+ return next;
4334
4331
  }
4335
- return route;
4332
+ return current;
4336
4333
  });
4337
- }, [currentRoute, updateTopRoute]);
4334
+ }, []);
4338
4335
 
4339
4336
  const handleListScroll = useCallback((event: {
4340
4337
  stopPropagation: () => void;
@@ -4708,6 +4705,29 @@ export function CommandBar({
4708
4705
  disconnectBrokerInstance,
4709
4706
  executeCollectionCommand,
4710
4707
  ]);
4708
+ const activateListSelectionRef = useRef(activateListSelection);
4709
+ activateListSelectionRef.current = activateListSelection;
4710
+
4711
+ const handleListRowMouseDown = useCallback((event: any, item: ResultItem, globalIdx: number) => {
4712
+ event.stopPropagation?.();
4713
+ event.preventDefault?.();
4714
+ if (!currentRouteRef.current) {
4715
+ setRootSelectedIdx((current) => (current === globalIdx ? current : globalIdx));
4716
+ } else {
4717
+ setRouteStack((current) => {
4718
+ if (current.length === 0) return current;
4719
+ const next = [...current];
4720
+ const route = next[next.length - 1];
4721
+ if (!route || (route.kind !== "mode" && route.kind !== "picker" && route.kind !== "pane-settings")) {
4722
+ return current;
4723
+ }
4724
+ if (route.selectedIdx === globalIdx && route.hoveredIdx === globalIdx) return current;
4725
+ next[next.length - 1] = { ...route, selectedIdx: globalIdx, hoveredIdx: globalIdx };
4726
+ return next;
4727
+ });
4728
+ }
4729
+ activateListSelectionRef.current({ item });
4730
+ }, []);
4711
4731
 
4712
4732
  const confirmCurrentRoute = useCallback(async () => {
4713
4733
  if (currentRoute?.kind !== "confirm") return;
@@ -4845,6 +4865,7 @@ export function CommandBar({
4845
4865
  preventDefault: () => void;
4846
4866
  }) => void) => void;
4847
4867
  };
4868
+ if (!keyInput) return undefined;
4848
4869
  const handleKeyPress = (event: {
4849
4870
  name: string;
4850
4871
  sequence?: string;
@@ -5041,6 +5062,27 @@ export function CommandBar({
5041
5062
  return;
5042
5063
  }
5043
5064
 
5065
+ if (themePickerActive) {
5066
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5067
+ event.stopPropagation();
5068
+ event.preventDefault();
5069
+ themePickerRef.current?.move(-1);
5070
+ return;
5071
+ }
5072
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5073
+ event.stopPropagation();
5074
+ event.preventDefault();
5075
+ themePickerRef.current?.move(1);
5076
+ return;
5077
+ }
5078
+ if (event.name === "return" || event.name === "enter") {
5079
+ event.stopPropagation();
5080
+ event.preventDefault();
5081
+ themePickerRef.current?.commit();
5082
+ return;
5083
+ }
5084
+ }
5085
+
5044
5086
  const activeListState = visibleListStateRef.current;
5045
5087
  if (!activeListState) return;
5046
5088
 
@@ -5108,13 +5150,13 @@ export function CommandBar({
5108
5150
  if (keyInput.onInternal) {
5109
5151
  keyInput.onInternal("keypress", handleKeyPress);
5110
5152
  } else {
5111
- renderer.keyInput.on("keypress", handleKeyPress);
5153
+ keyInput.on("keypress", handleKeyPress);
5112
5154
  }
5113
5155
  return () => {
5114
5156
  if (keyInput.offInternal) {
5115
5157
  keyInput.offInternal("keypress", handleKeyPress);
5116
5158
  } else {
5117
- renderer.keyInput.off("keypress", handleKeyPress);
5159
+ keyInput.off("keypress", handleKeyPress);
5118
5160
  }
5119
5161
  };
5120
5162
  }, [
@@ -5134,6 +5176,7 @@ export function CommandBar({
5134
5176
  renderer,
5135
5177
  rootModeInfo.kind,
5136
5178
  setActiveListQuery,
5179
+ themePickerActive,
5137
5180
  updateWorkflowValue,
5138
5181
  ]);
5139
5182
 
@@ -5155,8 +5198,23 @@ export function CommandBar({
5155
5198
  ? routeListState
5156
5199
  : null;
5157
5200
  const showCustomMultiSelectPicker = currentRoute?.kind === "picker" && currentRoute.pickerId === "field-multi-select";
5158
- const listRows = visibleListState ? buildListRows(visibleListState) : [];
5159
- const nativeListRows = visibleListState ? buildNativeListRows(visibleListState, listRows) : [];
5201
+ const listRows = useMemo(
5202
+ () => (visibleListState ? buildListRows(visibleListState) : []),
5203
+ [visibleListState?.results],
5204
+ );
5205
+ const nativeListRows = useMemo(
5206
+ () => (visibleListState ? buildNativeListRows(visibleListState, listRows) : []),
5207
+ [listRows, visibleListState?.emptyLabel, visibleListState?.searching],
5208
+ );
5209
+ const listRowIndexByGlobalIndex = useMemo(() => {
5210
+ const indexByGlobalIndex = new Map<number, number>();
5211
+ listRows.forEach((row, index) => {
5212
+ if (row.kind === "item") {
5213
+ indexByGlobalIndex.set(row.globalIdx, index);
5214
+ }
5215
+ });
5216
+ return indexByGlobalIndex;
5217
+ }, [listRows]);
5160
5218
  const workflowBodyHeight = currentRoute?.kind === "workflow"
5161
5219
  ? Math.min(
5162
5220
  Math.max(9, termHeight - (nativePaneChrome ? 7 : 9)),
@@ -5185,26 +5243,30 @@ export function CommandBar({
5185
5243
  const trailingWidth = Math.max(8, Math.min(12, Math.floor(resultsInnerWidth * 0.18)));
5186
5244
  const labelWidth = Math.max(10, resultsInnerWidth - trailingWidth);
5187
5245
  const queryDisplayWidth = Math.max(8, resultsInnerWidth);
5188
- const nativeSelectedRowIndex = nativePaneChrome && visibleListState
5189
- ? nativeListRows.findIndex((row) => row.kind === "item" && row.globalIdx === visibleListState.selectedIdx)
5246
+ const selectedListRowIndex = visibleListState
5247
+ ? listRowIndexByGlobalIndex.get(visibleListState.selectedIdx) ?? -1
5190
5248
  : -1;
5249
+ const selectedScrollRowIndex = selectedListRowIndex;
5191
5250
  const bodySlotKey = showCustomMultiSelectPicker
5192
5251
  ? "picker:field-multi-select"
5193
- : currentRoute?.kind === "picker"
5194
- ? `picker:${currentRoute.pickerId}`
5195
- : currentRoute?.kind ?? "root";
5252
+ : themePickerActive
5253
+ ? "theme-picker"
5254
+ : currentRoute?.kind === "picker"
5255
+ ? `picker:${currentRoute.pickerId}`
5256
+ : currentRoute?.kind ?? "root";
5196
5257
 
5197
- useEffect(() => {
5198
- if (!nativePaneChrome || nativeSelectedRowIndex < 0) return;
5258
+ useLayoutEffect(() => {
5199
5259
  const scrollBox = nativeListScrollRef.current;
5200
5260
  if (!scrollBox) return;
5261
+ if (scrollBox.verticalScrollBar) scrollBox.verticalScrollBar.visible = false;
5262
+ if (selectedScrollRowIndex < 0) return;
5201
5263
  const viewportHeight = Math.max(1, scrollBox.viewport?.height ?? listBodyHeight);
5202
- if (nativeSelectedRowIndex < scrollBox.scrollTop) {
5203
- scrollBox.scrollTo(nativeSelectedRowIndex);
5204
- } else if (nativeSelectedRowIndex >= scrollBox.scrollTop + viewportHeight) {
5205
- scrollBox.scrollTo(nativeSelectedRowIndex - viewportHeight + 1);
5264
+ if (selectedScrollRowIndex < scrollBox.scrollTop) {
5265
+ scrollBox.scrollTo(selectedScrollRowIndex);
5266
+ } else if (selectedScrollRowIndex >= scrollBox.scrollTop + viewportHeight) {
5267
+ scrollBox.scrollTo(selectedScrollRowIndex - viewportHeight + 1);
5206
5268
  }
5207
- }, [listBodyHeight, nativePaneChrome, nativeSelectedRowIndex, visibleListState?.kind, visibleListState?.query]);
5269
+ }, [listBodyHeight, selectedScrollRowIndex, visibleListState?.kind, visibleListState?.query]);
5208
5270
 
5209
5271
  useEffect(() => {
5210
5272
  if (!nativePaneChrome || currentRoute?.kind !== "workflow") return;
@@ -5225,140 +5287,6 @@ export function CommandBar({
5225
5287
  }
5226
5288
  }, [bodyHeight, currentRoute, nativePaneChrome]);
5227
5289
 
5228
- const renderListBody = () => {
5229
- if (!visibleListState) return null;
5230
-
5231
- const allRows = listRows;
5232
- let visibleRows: CommandBarListRow[];
5233
- if (nativePaneChrome) {
5234
- visibleRows = nativeListRows;
5235
- } else if (visibleListState.searching && allRows.length === 0) {
5236
- visibleRows = [{ kind: "spinner", id: "searching", label: "Searching…" }];
5237
- } else if (allRows.length === 0) {
5238
- visibleRows = [{ kind: "message", id: "empty", label: visibleListState.emptyLabel }];
5239
- } else {
5240
- const selectedRowIdx = allRows.findIndex((row) => row.kind === "item" && row.globalIdx === visibleListState.selectedIdx);
5241
- const halfWindow = Math.floor(listBodyHeight / 2);
5242
- let windowStart = Math.max(0, Math.min(selectedRowIdx - halfWindow, allRows.length - listBodyHeight));
5243
- if (windowStart < 0) windowStart = 0;
5244
- visibleRows = allRows.slice(windowStart, windowStart + listBodyHeight);
5245
- if (visibleListState.searching) {
5246
- if (visibleRows.length >= listBodyHeight) {
5247
- visibleRows = visibleRows.slice(0, listBodyHeight - 1);
5248
- }
5249
- visibleRows.push({ kind: "spinner", id: "searching", label: "Searching…" });
5250
- }
5251
- }
5252
-
5253
- while (!nativePaneChrome && visibleRows.length < listBodyHeight) {
5254
- visibleRows.push({ kind: "filler", id: `filler:${visibleRows.length}` });
5255
- }
5256
-
5257
- const renderedRows = (
5258
- <>
5259
- {visibleRows.map((row) => {
5260
- if (row.kind === "filler" || row.kind === "spacer") {
5261
- return <Box key={row.id} height={1} />;
5262
- }
5263
- if (row.kind === "spinner") {
5264
- return (
5265
- <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: handleListScroll } : {})}>
5266
- <Spinner label={row.label} />
5267
- </Box>
5268
- );
5269
- }
5270
- if (row.kind === "message") {
5271
- return (
5272
- <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: handleListScroll } : {})}>
5273
- <Text fg={paletteText}>{truncateText(row.label, queryDisplayWidth)}</Text>
5274
- </Box>
5275
- );
5276
- }
5277
- if (row.kind === "heading") {
5278
- return (
5279
- <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: handleListScroll } : {})}>
5280
- <Text attributes={TextAttributes.BOLD} fg={paletteHeadingText}>
5281
- {truncateText(row.label, queryDisplayWidth)}
5282
- </Text>
5283
- </Box>
5284
- );
5285
- }
5286
-
5287
- const isSelected = row.globalIdx === visibleListState.selectedIdx;
5288
- const isHovered = row.globalIdx === visibleListState.hoveredIdx && !isSelected;
5289
- const presentation = getRowPresentation(row.item, isSelected, trailingWidth > 0);
5290
- const label = truncateText(presentation.label, labelWidth);
5291
- const trailing = truncateText(presentation.trailing, trailingWidth);
5292
- return (
5293
- <Box
5294
- key={row.item.id}
5295
- flexDirection="row"
5296
- height={1}
5297
- paddingX={contentPadding}
5298
- backgroundColor={isSelected
5299
- ? paletteSelectedBg
5300
- : isHovered
5301
- ? paletteHoverBg
5302
- : (nativePaneChrome ? panelBg : paletteBg)}
5303
- onMouseMove={() => setHoveredIndex(row.globalIdx)}
5304
- onMouseOut={() => setHoveredIndex(null)}
5305
- {...(!nativePaneChrome ? { onMouseScroll: handleListScroll } : {})}
5306
- onMouseDown={(event: any) => {
5307
- event.stopPropagation?.();
5308
- event.preventDefault?.();
5309
- if (!currentRoute) {
5310
- setRootSelectedIdx(row.globalIdx);
5311
- } else {
5312
- updateTopRoute((route) => {
5313
- if (route.kind === "mode" || route.kind === "picker" || route.kind === "pane-settings") {
5314
- return { ...route, selectedIdx: row.globalIdx, hoveredIdx: row.globalIdx };
5315
- }
5316
- return route;
5317
- });
5318
- }
5319
- activateListSelection({ item: row.item });
5320
- }}
5321
- data-command-bar-row-selected={nativePaneChrome && isSelected ? "true" : undefined}
5322
- style={nativePaneChrome ? { borderRadius: 6 } : undefined}
5323
- >
5324
- <Box width={labelWidth}>
5325
- <Text fg={isSelected ? paletteSelectedText : presentation.primaryMuted ? paletteSubtleText : paletteText}>
5326
- {label}
5327
- </Text>
5328
- </Box>
5329
- <Box width={trailingWidth}>
5330
- <Text fg={isSelected ? paletteSelectedText : paletteSubtleText}>{trailing}</Text>
5331
- </Box>
5332
- </Box>
5333
- );
5334
- })}
5335
- </>
5336
- );
5337
-
5338
- if (nativePaneChrome) {
5339
- return (
5340
- <ScrollBox
5341
- ref={nativeListScrollRef}
5342
- flexDirection="column"
5343
- height={listBodyHeight}
5344
- scrollY
5345
- >
5346
- {renderedRows}
5347
- </ScrollBox>
5348
- );
5349
- }
5350
-
5351
- return (
5352
- <Box
5353
- flexDirection="column"
5354
- height={listBodyHeight}
5355
- onMouseScroll={handleListScroll}
5356
- >
5357
- {renderedRows}
5358
- </Box>
5359
- );
5360
- };
5361
-
5362
5290
  const renderWorkflowBody = () => {
5363
5291
  if (currentRoute?.kind !== "workflow") return null;
5364
5292
  const visibleFields = getVisibleWorkflowFields(currentRoute.fields, currentRoute.values);
@@ -5436,7 +5364,7 @@ export function CommandBar({
5436
5364
  borderColor={active ? paletteSelectedBg : paletteBg}
5437
5365
  backgroundColor={nativePaneChrome ? inputBg : fieldBg}
5438
5366
  style={nativePaneChrome ? {
5439
- border: `1px solid ${active ? colors.borderFocused : colors.border}`,
5367
+ border: `1px solid ${active ? themeColors.borderFocused : themeColors.border}`,
5440
5368
  borderRadius: 6,
5441
5369
  overflow: "hidden",
5442
5370
  } : undefined}
@@ -5450,7 +5378,7 @@ export function CommandBar({
5450
5378
  focused={!currentRoute.pending}
5451
5379
  textColor={paletteText}
5452
5380
  placeholderColor={paletteSubtleText}
5453
- backgroundColor={nativePaneChrome ? inputBg : colors.panel}
5381
+ backgroundColor={nativePaneChrome ? inputBg : themeColors.panel}
5454
5382
  flexGrow={1}
5455
5383
  wrapText
5456
5384
  />
@@ -5531,7 +5459,7 @@ export function CommandBar({
5531
5459
  })}
5532
5460
  {currentRoute.error && (
5533
5461
  <Box height={1}>
5534
- <Text fg={colors.negative}>{truncateText(currentRoute.error, queryDisplayWidth)}</Text>
5462
+ <Text fg={themeColors.negative}>{truncateText(currentRoute.error, queryDisplayWidth)}</Text>
5535
5463
  </Box>
5536
5464
  )}
5537
5465
  {currentRoute.pendingLabel && currentRoute.pending && (
@@ -5574,33 +5502,14 @@ export function CommandBar({
5574
5502
  const renderConfirmBody = () => {
5575
5503
  if (currentRoute?.kind !== "confirm") return null;
5576
5504
  return (
5577
- <Box flexDirection="column" height={bodyHeight} paddingX={contentPadding}>
5578
- {currentRoute.body.map((line, index) => (
5579
- <Box key={`confirm:${index}`} height={1}>
5580
- <Text fg={paletteText}>{truncateText(line, queryDisplayWidth)}</Text>
5581
- </Box>
5582
- ))}
5583
- <Box height={1} />
5584
- {currentRoute.error && (
5585
- <Box height={1}>
5586
- <Text fg={colors.negative}>{truncateText(currentRoute.error, queryDisplayWidth)}</Text>
5587
- </Box>
5588
- )}
5589
- {currentRoute.pending && (
5590
- <Box height={1}>
5591
- <Spinner label="Working…" />
5592
- </Box>
5593
- )}
5594
- <Box flexGrow={1} />
5595
- <Box flexDirection="row" gap={1}>
5596
- <Button
5597
- label={currentRoute.confirmLabel}
5598
- variant={currentRoute.tone === "danger" ? "danger" : "primary"}
5599
- onPress={() => { void confirmCurrentRoute(); }}
5600
- disabled={currentRoute.pending}
5601
- />
5602
- </Box>
5603
- </Box>
5505
+ <CommandBarConfirmBody
5506
+ route={currentRoute}
5507
+ bodyHeight={bodyHeight}
5508
+ contentPadding={contentPadding}
5509
+ paletteText={paletteText}
5510
+ queryDisplayWidth={queryDisplayWidth}
5511
+ onConfirm={() => { void confirmCurrentRoute(); }}
5512
+ />
5604
5513
  );
5605
5514
  };
5606
5515
 
@@ -5691,10 +5600,9 @@ export function CommandBar({
5691
5600
  <Box flexGrow={1}>
5692
5601
  <Text fg={paletteText} attributes={TextAttributes.BOLD}>
5693
5602
  {currentRoute?.kind === "mode"
5694
- ? currentRoute.screen === "themes" ? "Change Theme"
5695
- : currentRoute.screen === "plugins" ? "Manage Plugins"
5696
- : currentRoute.screen === "layout" ? "Layout Actions"
5697
- : "Security Description"
5603
+ ? currentRoute.screen === "plugins" ? "Manage Plugins"
5604
+ : currentRoute.screen === "layout" ? "Layout Actions"
5605
+ : "Security Description"
5698
5606
  : currentRoute?.kind === "picker" ? currentRoute.title
5699
5607
  : currentRoute?.kind === "pane-settings" ? "Pane Settings"
5700
5608
  : currentRoute?.kind === "workflow" ? currentRoute.title
@@ -5723,61 +5631,80 @@ export function CommandBar({
5723
5631
  )}
5724
5632
  {(visibleListState || currentRoute?.kind === "picker") && visibleListState && (
5725
5633
  <>
5726
- <Box height={1} paddingX={contentPadding}>
5727
- <Box
5728
- width={queryDisplayWidth}
5729
- height={1}
5730
- position="relative"
5731
- backgroundColor={nativePaneChrome ? undefined : inputBg}
5732
- style={nativePaneChrome ? undefined : {
5733
- overflow: "hidden",
5734
- }}
5735
- >
5736
- <Input
5737
- value={visibleListState.query}
5738
- onInput={setActiveListQuery}
5739
- onChange={setActiveListQuery}
5740
- placeholder={visibleListState.kind === "root" ? "Search" : "Filter"}
5741
- focused
5742
- width={nativePaneChrome ? "100%" : queryDisplayWidth}
5743
- backgroundColor={nativePaneChrome ? "transparent" : paletteBg}
5744
- focusedBackgroundColor={nativePaneChrome ? "transparent" : paletteBg}
5745
- textColor={paletteText}
5746
- focusedTextColor={paletteText}
5747
- placeholderColor={paletteSubtleText}
5748
- cursorColor={colors.textBright}
5749
- />
5750
- {visibleListState.kind === "root" && rootGhostSuffix && (
5751
- <Box
5752
- position="absolute"
5753
- top={0}
5754
- left={Math.max(0, Math.min(rootQuery.length, queryDisplayWidth - 1))}
5755
- width={Math.max(0, queryDisplayWidth - Math.min(rootQuery.length, queryDisplayWidth - 1))}
5756
- height={1}
5757
- >
5758
- <Text fg={paletteSubtleText}>
5759
- {truncateText(
5760
- rootGhostSuffix,
5761
- Math.max(0, queryDisplayWidth - Math.min(rootQuery.length, queryDisplayWidth - 1)),
5762
- )}
5763
- </Text>
5764
- </Box>
5765
- )}
5766
- </Box>
5767
- </Box>
5768
- <Box height={1} paddingX={contentPadding}>
5769
- {visibleListState.kind === "root" && rootShortcutFeedback
5770
- ? (
5771
- <Text fg={paletteSubtleText}>
5772
- {truncateText(rootShortcutFeedback, queryDisplayWidth)}
5773
- </Text>
5774
- )
5775
- : null}
5776
- </Box>
5634
+ <CommandBarListHeader
5635
+ kind={visibleListState.kind}
5636
+ query={visibleListState.query}
5637
+ queryDisplayWidth={queryDisplayWidth}
5638
+ nativePaneChrome={nativePaneChrome}
5639
+ inputBg={inputBg}
5640
+ paletteBg={paletteBg}
5641
+ paletteText={paletteText}
5642
+ paletteSubtleText={paletteSubtleText}
5643
+ cursorColor={themeColors.textBright}
5644
+ contentPadding={contentPadding}
5645
+ rootGhostSuffix={rootGhostSuffix}
5646
+ rootQueryLength={rootQuery.length}
5647
+ rootShortcutFeedback={rootShortcutFeedback}
5648
+ onQueryChange={setActiveListQuery}
5649
+ />
5777
5650
  </>
5778
5651
  )}
5779
5652
 
5780
- {visibleListState && !showCustomMultiSelectPicker && renderListBody()}
5653
+ {themePickerActive && (
5654
+ <ThemePicker
5655
+ ref={themePickerRef}
5656
+ filter={themePickerFilter}
5657
+ committedThemeId={state.config.theme}
5658
+ height={listBodyHeight}
5659
+ contentPadding={contentPadding}
5660
+ labelWidth={labelWidth}
5661
+ trailingWidth={trailingWidth}
5662
+ queryDisplayWidth={queryDisplayWidth}
5663
+ nativePaneChrome={nativePaneChrome}
5664
+ paletteBg={paletteBg}
5665
+ paletteHoverBg={paletteHoverBg}
5666
+ paletteSelectedBg={paletteSelectedBg}
5667
+ paletteSelectedText={paletteSelectedText}
5668
+ paletteSubtleText={paletteSubtleText}
5669
+ paletteText={paletteText}
5670
+ panelBg={panelBg}
5671
+ onPreview={applyThemePreview}
5672
+ onCommit={(themeId) => {
5673
+ const nextConfig = {
5674
+ ...stateRef.current.config,
5675
+ theme: themeId,
5676
+ };
5677
+ commitTheme(themeId);
5678
+ void saveConfig(nextConfig);
5679
+ closeAll({ revertThemePreview: false });
5680
+ }}
5681
+ />
5682
+ )}
5683
+
5684
+ {visibleListState && !themePickerActive && !showCustomMultiSelectPicker && (
5685
+ <CommandBarListBody
5686
+ visibleListState={visibleListState}
5687
+ nativeListRows={nativeListRows}
5688
+ listBodyHeight={listBodyHeight}
5689
+ contentPadding={contentPadding}
5690
+ labelWidth={labelWidth}
5691
+ nativePaneChrome={nativePaneChrome}
5692
+ nativeListScrollRef={nativeListScrollRef}
5693
+ paletteBg={paletteBg}
5694
+ paletteHeadingText={paletteHeadingText}
5695
+ paletteHoverBg={paletteHoverBg}
5696
+ paletteSelectedBg={paletteSelectedBg}
5697
+ paletteSelectedText={paletteSelectedText}
5698
+ paletteSubtleText={paletteSubtleText}
5699
+ paletteText={paletteText}
5700
+ panelBg={panelBg}
5701
+ queryDisplayWidth={queryDisplayWidth}
5702
+ trailingWidth={trailingWidth}
5703
+ onHoverIndex={setHoveredIndex}
5704
+ onListScroll={handleListScroll}
5705
+ onRowMouseDown={handleListRowMouseDown}
5706
+ />
5707
+ )}
5781
5708
  {currentRoute?.kind === "workflow" && renderWorkflowBody()}
5782
5709
  {currentRoute?.kind === "confirm" && renderConfirmBody()}
5783
5710
  {showCustomMultiSelectPicker && renderMultiSelectBody()}