gloomberb 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/package.json +2 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +62 -77
  14. package/src/components/chart/native/kitty.test.ts +23 -0
  15. package/src/components/chart/native/surface-visibility.ts +48 -0
  16. package/src/components/chart/static-chart-surface.tsx +28 -4
  17. package/src/components/chart/stock-chart.test.ts +0 -16
  18. package/src/components/chart/stock-chart.tsx +52 -71
  19. package/src/components/chart/time-axis-label.tsx +43 -0
  20. package/src/components/command-bar/command-bar.test.tsx +71 -8
  21. package/src/components/command-bar/command-bar.tsx +388 -713
  22. package/src/components/command-bar/confirm-body.tsx +51 -0
  23. package/src/components/command-bar/list-model.ts +77 -0
  24. package/src/components/command-bar/view-model.ts +0 -19
  25. package/src/components/command-bar/workflow-ops.ts +4 -0
  26. package/src/components/data-table-stack-view.test.tsx +39 -4
  27. package/src/components/data-table-stack-view.tsx +11 -104
  28. package/src/components/data-table-view.tsx +43 -79
  29. package/src/components/feed-data-table-stack-view.tsx +10 -17
  30. package/src/components/feed-stack-controller.ts +39 -0
  31. package/src/components/index.ts +6 -2
  32. package/src/components/layout/floating-pane.tsx +30 -2
  33. package/src/components/layout/header.tsx +10 -7
  34. package/src/components/layout/pane-content.tsx +2 -0
  35. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  36. package/src/components/layout/pane-cycle-order.ts +32 -0
  37. package/src/components/layout/pane-footer.tsx +26 -4
  38. package/src/components/layout/pane-header.tsx +5 -5
  39. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  40. package/src/components/layout/shell.test.tsx +66 -22
  41. package/src/components/layout/shell.tsx +13 -6
  42. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  43. package/src/components/pane-settings-dialog.tsx +12 -4
  44. package/src/components/pane-template-wizard.test.tsx +3 -4
  45. package/src/components/speedometer-gauge.test.tsx +45 -0
  46. package/src/components/speedometer-gauge.tsx +558 -0
  47. package/src/components/table-view-shared.tsx +158 -0
  48. package/src/components/ticker-list-table-view.tsx +42 -72
  49. package/src/components/ui/confirm-dialog.tsx +66 -0
  50. package/src/components/ui/data-table.tsx +18 -10
  51. package/src/components/ui/index.ts +0 -1
  52. package/src/components/ui/page-stack-view.tsx +39 -10
  53. package/src/components/ui/remote-image.tsx +15 -40
  54. package/src/components/ui/table-layout.test.ts +16 -0
  55. package/src/components/ui/table-layout.ts +10 -3
  56. package/src/components/ui/ui.test.tsx +100 -20
  57. package/src/core/state/app-state.ts +6 -3
  58. package/src/market-data/coordinator.test.ts +1 -1
  59. package/src/market-data/coordinator.ts +0 -1
  60. package/src/news/aggregator.ts +0 -3
  61. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  62. package/src/plugins/builtin/ai/providers.ts +0 -17
  63. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  64. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  65. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  66. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  67. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  68. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  69. package/src/plugins/builtin/chat-controller.ts +75 -6
  70. package/src/plugins/builtin/chat.test.tsx +6 -6
  71. package/src/plugins/builtin/chat.tsx +29 -20
  72. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  73. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  74. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  75. package/src/plugins/builtin/correlation/index.tsx +1 -6
  76. package/src/plugins/builtin/debug.tsx +1 -3
  77. package/src/plugins/builtin/econ/index.tsx +1 -2
  78. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  79. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  80. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  81. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  82. package/src/plugins/builtin/help.test.tsx +2 -2
  83. package/src/plugins/builtin/help.tsx +2 -2
  84. package/src/plugins/builtin/holders/index.tsx +10 -15
  85. package/src/plugins/builtin/insider/index.tsx +22 -89
  86. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  87. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  88. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  89. package/src/plugins/builtin/news.tsx +4 -16
  90. package/src/plugins/builtin/notes.tsx +18 -55
  91. package/src/plugins/builtin/options.test.tsx +58 -2
  92. package/src/plugins/builtin/options.tsx +108 -49
  93. package/src/plugins/builtin/plugin-groups.ts +2 -0
  94. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  95. package/src/plugins/builtin/research/index.tsx +5 -21
  96. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  97. package/src/plugins/builtin/sec.tsx +13 -66
  98. package/src/plugins/builtin/sectors/index.tsx +1 -1
  99. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  100. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  102. package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
  103. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
  104. package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
  105. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  106. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  107. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  108. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  109. package/src/plugins/ibkr/gateway-service.ts +1 -1
  110. package/src/plugins/ibkr/index.test.tsx +0 -2
  111. package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
  112. package/src/plugins/ibkr/trade-tab.tsx +13 -11
  113. package/src/plugins/pane-manager.ts +0 -13
  114. package/src/plugins/plugin-runtime.test.tsx +6 -6
  115. package/src/plugins/plugin-runtime.tsx +13 -8
  116. package/src/plugins/prediction-markets/cli.ts +0 -1
  117. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  118. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  119. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  120. package/src/plugins/registry.ts +10 -39
  121. package/src/react/input.ts +11 -2
  122. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  123. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  124. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  125. package/src/renderers/electrobun/view/input-host.tsx +123 -12
  126. package/src/renderers/electrobun/view/key-event.ts +1 -1
  127. package/src/renderers/electrobun/view/styles.css +331 -0
  128. package/src/renderers/electrobun/view/ui-host.tsx +22 -3
  129. package/src/renderers/opentui/chart-surface.tsx +179 -0
  130. package/src/renderers/opentui/host.tsx +6 -0
  131. package/src/renderers/opentui/image-loader.test.ts +25 -0
  132. package/src/renderers/opentui/image-loader.ts +105 -0
  133. package/src/renderers/opentui/image-surface.tsx +266 -0
  134. package/src/renderers/opentui/input-host.tsx +45 -6
  135. package/src/renderers/opentui/start.tsx +48 -30
  136. package/src/renderers/opentui/test-utils.tsx +17 -0
  137. package/src/renderers/opentui/ui-host.tsx +7 -3
  138. package/src/sources/provider-router.ts +0 -4
  139. package/src/state/app-context.test.ts +45 -0
  140. package/src/state/app-context.tsx +4 -0
  141. package/src/state/pane-scroll-registry.tsx +254 -0
  142. package/src/state/use-inline-tickers.test.tsx +73 -0
  143. package/src/state/use-inline-tickers.ts +7 -1
  144. package/src/test-support/plugin-runtime.ts +3 -2
  145. package/src/theme/colors.ts +12 -3
  146. package/src/theme/themes.test.ts +23 -1
  147. package/src/theme/themes.ts +115 -0
  148. package/src/types/broker.ts +1 -1
  149. package/src/types/data-provider.ts +0 -1
  150. package/src/types/plugin.ts +0 -2
  151. package/src/ui/host.tsx +4 -0
  152. package/src/ui/index.tsx +9 -1
  153. package/src/utils/back-navigation.ts +24 -1
  154. package/src/version.ts +1 -1
  155. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  156. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -71,12 +71,20 @@ import {
71
71
  import { notifyGridlockComplete } from "../../plugins/gridlock-notification";
72
72
  import { CHART_RENDERER_PREFERENCES } from "../chart/chart-types";
73
73
  import {
74
- buildSections,
75
74
  getEmptyState,
76
75
  getRowPresentation,
77
76
  resolveCommandBarMode,
78
77
  truncateText,
79
78
  } from "./view-model";
79
+ import {
80
+ buildListRows,
81
+ buildNativeListRows,
82
+ orderListResults,
83
+ type CommandBarListRow,
84
+ type ListScreenState,
85
+ type ResultItem,
86
+ } from "./list-model";
87
+ import { CommandBarConfirmBody } from "./confirm-body";
80
88
  import {
81
89
  createLocalTickerSearchCandidates,
82
90
  normalizeTickerInput,
@@ -156,49 +164,16 @@ interface CommandBarProps {
156
164
  onCheckForUpdates?: () => void | Promise<void>;
157
165
  }
158
166
 
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
167
  type WorkflowStringValues = Record<string, string>;
194
168
 
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 };
169
+ const LAYOUT_MOVE_ACTIONS = [
170
+ { id: "layout-move-left", label: "Move Left", direction: "left", unavailableDetail: "No column to the left" },
171
+ { id: "layout-move-right", label: "Move Right", direction: "right", unavailableDetail: "No column to the right" },
172
+ { id: "layout-move-up", label: "Move Up", direction: "above", unavailableDetail: "No pane above" },
173
+ { id: "layout-move-down", label: "Move Down", direction: "below", unavailableDetail: "No pane below" },
174
+ ] as const;
175
+
176
+ type LayoutMoveDirection = (typeof LAYOUT_MOVE_ACTIONS)[number]["direction"];
202
177
 
203
178
  const commandBarLog = debugLog.createLogger("command-bar");
204
179
  const COMMAND_BAR_OVERLAY_Z_INDEX = 2_147_483_646;
@@ -286,40 +261,6 @@ function estimateWorkflowBodyRows(route: CommandBarWorkflowRoute): number {
286
261
  return introRows + fieldRows + statusRows + 1;
287
262
  }
288
263
 
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
264
  function clampListIndex(index: number, length: number): number {
324
265
  return Math.max(0, Math.min(index, Math.max(0, length - 1)));
325
266
  }
@@ -1220,6 +1161,344 @@ export function CommandBar({
1220
1161
  });
1221
1162
  }, [openConfirmRoute]);
1222
1163
 
1164
+ const buildThemeItems = useCallback((
1165
+ query: string,
1166
+ currentThemeId: string | null | undefined,
1167
+ options?: { onSelectTheme?: (themeId: string) => void },
1168
+ ): { items: ResultItem[]; selectedIdx: number } => {
1169
+ let selectedIdx = 0;
1170
+ const filtered = query
1171
+ ? getThemeOptions().filter((theme) => (
1172
+ theme.name.toLowerCase().includes(query.toLowerCase())
1173
+ || theme.id.includes(query.toLowerCase())
1174
+ ))
1175
+ : getThemeOptions();
1176
+ const items = filtered.map((theme, index): ResultItem => {
1177
+ const current = theme.id === currentThemeId;
1178
+ if (current) selectedIdx = index;
1179
+ return {
1180
+ id: `theme:${theme.id}`,
1181
+ label: theme.name,
1182
+ detail: theme.description,
1183
+ category: "Themes",
1184
+ kind: "theme",
1185
+ current,
1186
+ themeId: theme.id,
1187
+ action: () => {
1188
+ options?.onSelectTheme?.(theme.id);
1189
+ const nextConfig = {
1190
+ ...state.config,
1191
+ theme: theme.id,
1192
+ };
1193
+ dispatch({ type: "SET_THEME", theme: theme.id });
1194
+ void saveConfig(nextConfig);
1195
+ closeAll({ revertThemePreview: false });
1196
+ },
1197
+ };
1198
+ });
1199
+ return { items, selectedIdx };
1200
+ }, [closeAll, dispatch, state.config]);
1201
+
1202
+ const buildPluginItems = useCallback((query: string): ResultItem[] => {
1203
+ const disabledPlugins = state.config.disabledPlugins || [];
1204
+ const toggleable = [...pluginRegistry.allPlugins.values()].filter((plugin) => plugin.toggleable);
1205
+ const filtered = query
1206
+ ? toggleable.filter((plugin) => (
1207
+ plugin.name.toLowerCase().includes(query.toLowerCase())
1208
+ || plugin.id.includes(query.toLowerCase())
1209
+ ))
1210
+ : toggleable;
1211
+ return filtered.map((plugin): ResultItem => {
1212
+ const enabled = !disabledPlugins.includes(plugin.id);
1213
+ const toggleAction = () => {
1214
+ dispatch({ type: "TOGGLE_PLUGIN", pluginId: plugin.id });
1215
+ const nextDisabled = enabled
1216
+ ? [...disabledPlugins, plugin.id]
1217
+ : disabledPlugins.filter((entry) => entry !== plugin.id);
1218
+ if (enabled) {
1219
+ for (const paneId of pluginRegistry.getPluginPaneIds(plugin.id)) {
1220
+ pluginRegistry.hidePane(paneId);
1221
+ }
1222
+ }
1223
+ void saveConfig({ ...state.config, disabledPlugins: nextDisabled });
1224
+ };
1225
+ return {
1226
+ id: `plugin:${plugin.id}`,
1227
+ label: plugin.name,
1228
+ detail: plugin.description || "",
1229
+ category: "Plugins",
1230
+ kind: "plugin",
1231
+ checked: enabled,
1232
+ pluginToggle: toggleAction,
1233
+ action: toggleAction,
1234
+ };
1235
+ });
1236
+ }, [dispatch, pluginRegistry, state.config]);
1237
+
1238
+ const buildLayoutItems = useCallback((
1239
+ query: string,
1240
+ options?: { confirmDangerousActions?: boolean },
1241
+ ): ResultItem[] => {
1242
+ const currentLayout = state.config.layout;
1243
+ const focusedPane = state.focusedPaneId ? findPaneInstance(currentLayout, state.focusedPaneId) : null;
1244
+ const focusedPaneDef = focusedPane ? pluginRegistry.panes.get(focusedPane.paneId) : null;
1245
+ const dockedPaneIds = getDockedPaneIds(currentLayout);
1246
+ const layoutHistory = state.layoutHistory[state.config.activeLayoutIndex];
1247
+ const layoutSnapshot = JSON.stringify(currentLayout);
1248
+ const canMove = (direction: LayoutMoveDirection) => (
1249
+ !!focusedPane && JSON.stringify(movePaneRelative(currentLayout, focusedPane.instanceId, direction)) !== layoutSnapshot
1250
+ );
1251
+ const layoutItems: ResultItem[] = [];
1252
+
1253
+ if (focusedPane && focusedPaneDef) {
1254
+ const focusedFloating = currentLayout.floating.find((entry) => entry.instanceId === focusedPane.instanceId);
1255
+ layoutItems.push({
1256
+ id: "layout-toggle-mode",
1257
+ label: focusedFloating ? "Dock Pane" : "Float Pane",
1258
+ detail: focusedFloating ? "Return the focused window to the layout" : "Detach the focused pane into a floating window",
1259
+ category: "Focused Pane",
1260
+ kind: "action",
1261
+ action: () => {
1262
+ const { width, height } = pluginRegistry.getTermSizeFn();
1263
+ const nextLayout = focusedFloating
1264
+ ? dockPane(currentLayout, focusedPane.instanceId)
1265
+ : floatPane(currentLayout, focusedPane.instanceId, width, height, focusedPaneDef);
1266
+ persistLayoutChange(nextLayout);
1267
+ closeAll({ revertThemePreview: false });
1268
+ },
1269
+ });
1270
+
1271
+ LAYOUT_MOVE_ACTIONS.forEach((entry) => {
1272
+ const available = canMove(entry.direction);
1273
+ layoutItems.push({
1274
+ id: entry.id,
1275
+ label: entry.label,
1276
+ detail: available ? "Reposition the focused pane" : entry.unavailableDetail,
1277
+ category: "Focused Pane",
1278
+ kind: available ? "action" : "info",
1279
+ disabled: !available,
1280
+ action: available
1281
+ ? () => {
1282
+ persistLayoutChange(movePaneRelative(currentLayout, focusedPane.instanceId, entry.direction));
1283
+ closeAll({ revertThemePreview: false });
1284
+ }
1285
+ : () => {},
1286
+ });
1287
+ });
1288
+
1289
+ layoutItems.push({
1290
+ id: "layout-swap",
1291
+ label: "Swap With…",
1292
+ detail: dockedPaneIds.length + currentLayout.floating.length > 1
1293
+ ? "Choose another pane to swap positions"
1294
+ : "Need at least two panes",
1295
+ category: "Focused Pane",
1296
+ kind: "action",
1297
+ disabled: dockedPaneIds.length + currentLayout.floating.length <= 1,
1298
+ action: () => {
1299
+ const pickerOptions = [
1300
+ ...dockedPaneIds,
1301
+ ...currentLayout.floating.map((entry) => entry.instanceId),
1302
+ ]
1303
+ .filter((paneId) => paneId !== focusedPane.instanceId)
1304
+ .map((paneId) => {
1305
+ const instance = findPaneInstance(currentLayout, paneId)!;
1306
+ const isFloating = currentLayout.floating.some((entry) => entry.instanceId === paneId);
1307
+ return {
1308
+ id: paneId,
1309
+ label: instance.title || pluginRegistry.panes.get(instance.paneId)?.name || instance.paneId,
1310
+ detail: isFloating ? "Floating window" : "Docked pane",
1311
+ description: isFloating ? "Floating window" : "Docked pane",
1312
+ };
1313
+ });
1314
+ if (pickerOptions.length === 0) return;
1315
+ openPickerRoute({
1316
+ kind: "picker",
1317
+ pickerId: "layout-swap",
1318
+ title: "Swap With…",
1319
+ query: "",
1320
+ selectedIdx: 0,
1321
+ hoveredIdx: null,
1322
+ options: pickerOptions,
1323
+ payload: { sourcePaneId: focusedPane.instanceId },
1324
+ });
1325
+ },
1326
+ });
1327
+
1328
+ layoutItems.push({
1329
+ id: "layout-duplicate",
1330
+ label: "Duplicate Pane",
1331
+ detail: "Create another instance next to the focused pane",
1332
+ category: "Focused Pane",
1333
+ kind: "action",
1334
+ action: () => {
1335
+ duplicatePane(focusedPane.instanceId);
1336
+ closeAll({ revertThemePreview: false });
1337
+ },
1338
+ });
1339
+
1340
+ layoutItems.push({
1341
+ id: "layout-close-pane",
1342
+ label: "Close Pane",
1343
+ detail: "Remove the focused pane from the layout",
1344
+ category: "Focused Pane",
1345
+ kind: "action",
1346
+ action: options?.confirmDangerousActions
1347
+ ? () => {
1348
+ openInlineConfirm({
1349
+ confirmId: "layout-close-pane",
1350
+ title: "Close Pane",
1351
+ body: [`Close "${focusedPane.title || focusedPaneDef.name || focusedPane.instanceId}"?`],
1352
+ confirmLabel: "Close Pane",
1353
+ cancelLabel: "Back",
1354
+ tone: "danger",
1355
+ onConfirm: () => {
1356
+ persistLayoutChange(removePane(currentLayout, focusedPane.instanceId));
1357
+ },
1358
+ });
1359
+ }
1360
+ : () => {
1361
+ persistLayoutChange(removePane(currentLayout, focusedPane.instanceId));
1362
+ closeAll({ revertThemePreview: false });
1363
+ },
1364
+ });
1365
+ } else {
1366
+ layoutItems.push({
1367
+ id: "layout-no-focused-pane",
1368
+ label: "No focused pane",
1369
+ detail: "Focus a pane to show pane-specific layout actions",
1370
+ category: "Focused Pane",
1371
+ kind: "info",
1372
+ action: () => {},
1373
+ });
1374
+ }
1375
+
1376
+ layoutItems.push({
1377
+ id: "layout-undo",
1378
+ label: "Undo Layout Change",
1379
+ detail: (layoutHistory?.past.length ?? 0) > 0 ? "Restore the previous layout state" : "No previous layout state",
1380
+ category: "Current Layout",
1381
+ kind: "action",
1382
+ disabled: (layoutHistory?.past.length ?? 0) === 0,
1383
+ action: () => {
1384
+ if ((layoutHistory?.past.length ?? 0) === 0) return;
1385
+ dispatch({ type: "UNDO_LAYOUT" });
1386
+ closeAll({ revertThemePreview: false });
1387
+ },
1388
+ });
1389
+ layoutItems.push({
1390
+ id: "layout-redo",
1391
+ label: "Redo Layout Change",
1392
+ detail: (layoutHistory?.future.length ?? 0) > 0 ? "Reapply the next layout state" : "No later layout state",
1393
+ category: "Current Layout",
1394
+ kind: "action",
1395
+ disabled: (layoutHistory?.future.length ?? 0) === 0,
1396
+ action: () => {
1397
+ if ((layoutHistory?.future.length ?? 0) === 0) return;
1398
+ dispatch({ type: "REDO_LAYOUT" });
1399
+ closeAll({ revertThemePreview: false });
1400
+ },
1401
+ });
1402
+ layoutItems.push({
1403
+ id: "layout-reset",
1404
+ label: "Reset Current Layout",
1405
+ detail: "Restore the default two-pane layout",
1406
+ category: "Current Layout",
1407
+ kind: "action",
1408
+ action: options?.confirmDangerousActions
1409
+ ? () => {
1410
+ openInlineConfirm({
1411
+ confirmId: "layout-reset",
1412
+ title: "Reset Current Layout",
1413
+ body: ["Reset the current layout to the default two-pane arrangement?"],
1414
+ confirmLabel: "Reset Layout",
1415
+ cancelLabel: "Back",
1416
+ tone: "danger",
1417
+ onConfirm: () => {
1418
+ persistLayoutChange(cloneLayout(DEFAULT_LAYOUT));
1419
+ },
1420
+ });
1421
+ }
1422
+ : () => {
1423
+ persistLayoutChange(cloneLayout(DEFAULT_LAYOUT));
1424
+ closeAll({ revertThemePreview: false });
1425
+ },
1426
+ });
1427
+ layoutItems.push({
1428
+ id: "layout-gridlock",
1429
+ label: "Gridlock All Windows",
1430
+ detail: currentLayout.floating.length > 0
1431
+ ? "Infer a tiled layout from the current window positions"
1432
+ : "Retile all panes from their current arrangement",
1433
+ category: "Current Layout",
1434
+ kind: "action",
1435
+ action: () => {
1436
+ const { width, height } = pluginRegistry.getTermSizeFn();
1437
+ persistLayoutChange(gridlockAllPanes(currentLayout, { x: 0, y: 0, width, height }));
1438
+ notifyGridlockRevert();
1439
+ closeAll({ revertThemePreview: false });
1440
+ },
1441
+ });
1442
+ layoutItems.push({
1443
+ id: "layout-rename",
1444
+ label: "Rename Layout",
1445
+ detail: "Change the current saved layout name",
1446
+ category: "Current Layout",
1447
+ kind: "action",
1448
+ action: () => openBuiltInWorkflow("rename-layout"),
1449
+ });
1450
+ layoutItems.push({
1451
+ id: "layout-duplicate-layout",
1452
+ label: "Duplicate Layout",
1453
+ detail: "Create a copy of the current layout",
1454
+ category: "Current Layout",
1455
+ kind: "action",
1456
+ action: () => {
1457
+ dispatch({ type: "DUPLICATE_LAYOUT", index: state.config.activeLayoutIndex });
1458
+ closeAll({ revertThemePreview: false });
1459
+ },
1460
+ });
1461
+ layoutItems.push({
1462
+ id: "layout-new",
1463
+ label: "New Layout",
1464
+ detail: "Create a fresh saved layout",
1465
+ category: "Current Layout",
1466
+ kind: "action",
1467
+ action: () => openBuiltInWorkflow("new-layout"),
1468
+ });
1469
+
1470
+ state.config.layouts.forEach((savedLayout, index) => {
1471
+ layoutItems.push({
1472
+ id: `layout-switch:${index}`,
1473
+ label: savedLayout.name,
1474
+ detail: index === state.config.activeLayoutIndex ? "Current layout" : "Switch to this saved layout",
1475
+ right: getLayoutPreview(savedLayout.layout),
1476
+ category: "Saved Layouts",
1477
+ kind: "action",
1478
+ current: index === state.config.activeLayoutIndex,
1479
+ action: () => {
1480
+ dispatch({ type: "SWITCH_LAYOUT", index });
1481
+ closeAll({ revertThemePreview: false });
1482
+ },
1483
+ });
1484
+ });
1485
+
1486
+ return query
1487
+ ? fuzzyFilter(layoutItems, query, (item) => `${item.label} ${item.detail} ${item.right || ""}`)
1488
+ : layoutItems;
1489
+ }, [
1490
+ closeAll,
1491
+ dispatch,
1492
+ duplicatePane,
1493
+ notifyGridlockRevert,
1494
+ openBuiltInWorkflow,
1495
+ openInlineConfirm,
1496
+ openPickerRoute,
1497
+ persistLayoutChange,
1498
+ pluginRegistry,
1499
+ state,
1500
+ ]);
1501
+
1223
1502
  const resolvePluginCommandConfirm = useCallback((command: CommandDef) => {
1224
1503
  const context = {
1225
1504
  config: state.config,
@@ -1987,10 +2266,15 @@ export function CommandBar({
1987
2266
  activeCollectionId,
1988
2267
  buildSharedWorkflowDeps(),
1989
2268
  );
1990
- await openPaneTemplateDirect(template, {
2269
+ const createOptions = {
1991
2270
  arg: trimmedList,
1992
2271
  symbols,
1993
- });
2272
+ };
2273
+ if (template.canCreate && !template.canCreate(getPaneTemplateContext(), createOptions)) {
2274
+ openPaneTemplateWorkflow(template, { arg: trimmedArg });
2275
+ return;
2276
+ }
2277
+ await openPaneTemplateDirect(template, createOptions);
1994
2278
  } catch {
1995
2279
  openPaneTemplateWorkflow(template, { arg: trimmedArg });
1996
2280
  }
@@ -2412,7 +2696,7 @@ export function CommandBar({
2412
2696
  switch (command.id) {
2413
2697
  case "help":
2414
2698
  closeAll({ revertThemePreview: false });
2415
- pluginRegistry.showWidget("help");
2699
+ pluginRegistry.showPane("help");
2416
2700
  return;
2417
2701
  case "pane-settings":
2418
2702
  if (state.focusedPaneId) openPaneSettingsRoute(state.focusedPaneId);
@@ -2959,321 +3243,18 @@ export function CommandBar({
2959
3243
  ) {
2960
3244
  items.push(shortcutItem);
2961
3245
  } 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
- };
2984
- 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,
2993
- });
2994
- }
3246
+ items.push(...buildPluginItems(match.arg));
2995
3247
  } 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));
3248
+ items.push(...buildLayoutItems(match.arg, { confirmDangerousActions: true }));
3245
3249
  } else if (match && match.command.id === "theme") {
3246
3250
  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
- });
3251
+ const themeResult = buildThemeItems(match.arg, savedThemeId, {
3252
+ onSelectTheme: (themeId) => {
3253
+ rootThemeBaseIdRef.current = themeId;
3254
+ },
3276
3255
  });
3256
+ initialIdx = themeResult.selectedIdx;
3257
+ items.push(...themeResult.items);
3277
3258
  } else if (match && match.command.id === "security-description") {
3278
3259
  if (shortcutItem) {
3279
3260
  items.push(shortcutItem);
@@ -3341,19 +3322,15 @@ export function CommandBar({
3341
3322
  activeTickerData,
3342
3323
  activeTickerSymbol,
3343
3324
  availableCommands,
3344
- closeAll,
3325
+ buildLayoutItems,
3326
+ buildPluginItems,
3327
+ buildThemeItems,
3345
3328
  createPluginCommandItem,
3346
3329
  currentRoute,
3347
- dispatch,
3348
- duplicatePane,
3349
3330
  executeCollectionCommand,
3350
3331
  localTickerSearchResultItems,
3351
3332
  nonShortcutPaneTemplateItems,
3352
- notifyGridlockRevert,
3353
- openBuiltInWorkflow,
3354
- openPickerRoute,
3355
3333
  paneShortcutItems,
3356
- persistLayoutChange,
3357
3334
  pluginCommandItems,
3358
3335
  pluginRegistry,
3359
3336
  rootModeInfo.kind,
@@ -3787,31 +3764,7 @@ export function CommandBar({
3787
3764
  if (currentRoute.kind === "mode") {
3788
3765
  switch (currentRoute.screen) {
3789
3766
  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
- }));
3767
+ const results = buildThemeItems(currentRoute.query, currentRoute.themeBaseId).items;
3815
3768
  return {
3816
3769
  kind: "mode",
3817
3770
  title: "Change Theme",
@@ -3828,39 +3781,7 @@ export function CommandBar({
3828
3781
  };
3829
3782
  }
3830
3783
  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
- });
3784
+ const results = buildPluginItems(currentRoute.query);
3864
3785
  return {
3865
3786
  kind: "mode",
3866
3787
  title: "Manage Plugins",
@@ -3877,237 +3798,7 @@ export function CommandBar({
3877
3798
  };
3878
3799
  }
3879
3800
  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
-
3801
+ const results = buildLayoutItems(currentRoute.query);
4111
3802
  return {
4112
3803
  kind: "mode",
4113
3804
  title: "Layout Actions",
@@ -4218,6 +3909,9 @@ export function CommandBar({
4218
3909
  activeMatch,
4219
3910
  activeTickerData,
4220
3911
  activeTickerSymbol,
3912
+ buildLayoutItems,
3913
+ buildPluginItems,
3914
+ buildThemeItems,
4221
3915
  closeAll,
4222
3916
  createPaneTemplateItem,
4223
3917
  currentRoute,
@@ -5574,33 +5268,14 @@ export function CommandBar({
5574
5268
  const renderConfirmBody = () => {
5575
5269
  if (currentRoute?.kind !== "confirm") return null;
5576
5270
  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>
5271
+ <CommandBarConfirmBody
5272
+ route={currentRoute}
5273
+ bodyHeight={bodyHeight}
5274
+ contentPadding={contentPadding}
5275
+ paletteText={paletteText}
5276
+ queryDisplayWidth={queryDisplayWidth}
5277
+ onConfirm={() => { void confirmCurrentRoute(); }}
5278
+ />
5604
5279
  );
5605
5280
  };
5606
5281