Search workspace files from the command center (#3059)

* feat(command-center): add workspace file search

Use the existing workspace-aware fuzzy search so file lookup follows the file explorer's ignore and hidden-directory rules. Cmd/Ctrl+P opens a removable Files scope while the normal command center also includes file matches.

* fix(command-center): stabilize file search results

* fix(shortcuts): preserve project picker command

* fix(command-center): keep file results while searching

* feat(command-center): integrate the files scope into the search row

The scope was a 24px pill with 12px text sitting inside the 16px input line, so
it read as a badge dropped into the query rather than the scope of the search.

Make it a leading cell of the search row instead: folder icon, input-sized label,
and a hairline divider before the caret. It stretches to the row's cross size and
bleeds through the header padding with negative margins, so its height is driven
entirely by the input and the panel's own radius clips its corner. Toggling the
scope cannot resize the header, which the new spec pins.

* fix(command-center): restore the files scope as a chip

The previous commit turned the scope into a flush leading cell of the header. Put
the chip back: rounded, bordered, folder icon and 14px label, no divider.

It stretches to the row instead of carrying a height, so it is exactly as tall as
the input's line box and cannot resize the header.

* refactor(shortcuts): regroup and reorder the shortcut help sections

Panels was a junk drawer: the command center, file search, the shortcuts sheet
itself, new terminal, the sidebars, settings, focus mode and theme cycling. The
two shortcuts people reach for most, Cmd+K and Cmd+P, sat inside it below three
other sections. Navigation held both workspace and tab movement.

Sections are now General, Projects & Workspaces, Tabs & Panes, Layout, and Agent
Input. General leads with the command center and file search. Tab movement joins
Tabs & Panes, new terminal joins the tabs it creates, and Layout keeps only the
sidebar and focus toggles.

Row order is now data rather than a side effect of binding-definition order, so
the row someone opens the sheet to find sits at the top of its section. A test
fails when a new shortcut has no place in that order instead of letting it sink
to the bottom unnoticed.

* Keep file search failures in view
This commit is contained in:
Mohamed Boudra
2026-08-10 17:02:35 +02:00
committed by GitHub
parent 42f1d0b289
commit a98d154b6b
26 changed files with 1070 additions and 141 deletions
@@ -0,0 +1,139 @@
import { expect, test } from "../support/fixtures";
import { openCommandCenter } from "../support/helpers/command-center";
import {
delayDirectorySuggestionResponses,
expectOneLineTruncatedFileResult,
expectStableCommandCenterLayout,
failDirectorySuggestionRequests,
startCommandCenterLayoutObservation,
} from "../support/helpers/command-center-file-search";
import { expectFileTabOpen } from "../support/helpers/file-explorer";
import { gotoWorkspace } from "../support/helpers/launcher";
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
const FILE_PATH =
"packages/app/src/command-center/features/workspace-file-search/layout-stability/needle-layout-stability-command-center.tsx";
const FILE_NAME = "needle-layout-stability-command-center.tsx";
const FILE_DIRECTORY =
"packages/app/src/command-center/features/workspace-file-search/layout-stability";
function paseoSizedFiles(): Array<{ path: string; content: string }> {
const files = Array.from({ length: 120 }, (_, index) => ({
path: `packages/app/src/features/feature-${String(index).padStart(3, "0")}/index.ts`,
content: `export const feature${index} = ${index};\n`,
}));
files.push({ path: FILE_PATH, content: "export const layoutNeedle = true;\n" });
return files;
}
async function seedMatchingWorkspaces(count: number): Promise<SeededWorkspace[]> {
return Promise.all(
Array.from({ length: count }, (_, index) =>
seedWorkspace({
repoPrefix: `command-center-file-search-error-${String(index).padStart(2, "0")}-`,
title: `Home failure ${String(index).padStart(2, "0")}`,
}),
),
);
}
test.use({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/145.0 Safari/537.36",
});
test("workspace file search stays geometrically stable through delayed loading and results", async ({
page,
}) => {
test.setTimeout(120_000);
const seeded = await seedWorkspace({
repoPrefix: "command-center-file-search-",
title: "Paseo-shaped file search",
repo: { files: paseoSizedFiles() },
});
try {
await delayDirectorySuggestionResponses(page, 800);
await gotoWorkspace(page, seeded.workspaceId);
await page.keyboard.press("Meta+P");
const panel = page.getByTestId("command-center-panel");
await expect(panel).toBeVisible({ timeout: 30_000 });
await expect(panel.getByRole("button", { name: "Files" })).toBeVisible();
await startCommandCenterLayoutObservation(page);
await panel.getByTestId("command-center-input").fill(FILE_NAME);
const row = panel.getByRole("button", {
name: `${FILE_NAME} ${FILE_DIRECTORY}`,
});
await expect(row).toBeVisible({ timeout: 30_000 });
await expectOneLineTruncatedFileResult(row, {
filename: FILE_NAME,
path: FILE_DIRECTORY,
});
await panel.getByTestId("command-center-input").fill("needle-layout");
await page.waitForTimeout(150);
expect(await row.isVisible()).toBe(true);
await expect(panel.getByTestId("command-center-file-search-loading")).toBeVisible();
await expect(panel.getByText("Searching files...", { exact: true })).toHaveCount(0);
await expect(panel.getByTestId("command-center-file-search-loading")).toBeHidden();
await expectStableCommandCenterLayout(page);
await row.click();
await expectFileTabOpen(page, FILE_PATH);
} finally {
await seeded.cleanup();
}
});
test("dropping the files scope leaves the search row the same height", async ({ page }) => {
const seeded = await seedWorkspace({
repoPrefix: "command-center-file-search-scope-",
title: "Scope chip height",
});
try {
await gotoWorkspace(page, seeded.workspaceId);
await page.keyboard.press("Meta+P");
const panel = page.getByTestId("command-center-panel");
await expect(panel).toBeVisible({ timeout: 30_000 });
const header = panel.getByTestId("command-center-header");
const scoped = await header.boundingBox();
await page.getByTestId("command-center-files-scope").click();
await expect(page.getByTestId("command-center-files-scope")).toHaveCount(0);
const unscoped = await header.boundingBox();
expect(scoped?.height).toBe(unscoped?.height);
} finally {
await seeded.cleanup();
}
});
test("unscoped command search keeps commands visible and reports file-search failures", async ({
page,
}) => {
test.setTimeout(120_000);
const seeded = await seedMatchingWorkspaces(9);
try {
await failDirectorySuggestionRequests(page);
await gotoWorkspace(page, seeded[0].workspaceId);
const panel = await openCommandCenter(page);
await panel.getByTestId("command-center-input").fill("home");
await expect(panel.getByRole("button", { name: "Home", exact: true })).toBeVisible();
await expect(panel.getByRole("button", { name: /Home failure 08/ })).toBeVisible();
await expect(panel.getByTestId("command-center-file-search-error")).toHaveText(
"Error: Test file search transport failure.",
);
await expect(panel.getByTestId("command-center-file-search-error")).toBeInViewport({
ratio: 1,
});
await expect(panel.getByText("No matches", { exact: true })).toHaveCount(0);
} finally {
await Promise.allSettled(seeded.map((workspace) => workspace.cleanup()));
}
});
@@ -0,0 +1,211 @@
import { expect, type Locator, type Page } from "@playwright/test";
import { daemonWsRoutePattern } from "./daemon-port";
type WebSocketMessage = string | Buffer;
interface SurfaceRect {
height: number;
width: number;
x: number;
y: number;
}
interface LayoutObservation {
active: boolean;
layoutShift: number;
samples: Record<string, SurfaceRect[]>;
}
type LayoutOracleWindow = Window & {
__commandCenterLayoutObservation?: LayoutObservation;
};
const SURFACE_SELECTORS = {
panel: '[data-testid="command-center-panel"]',
header: '[data-testid="command-center-header"]',
input: '[data-testid="command-center-input"]',
fileSearchStatus: '[data-testid="command-center-file-search-status"]',
results: '[data-testid="command-center-results"]',
scope: '[data-testid="command-center-files-scope"]',
fileRow: '[data-testid^="command-center-file-row-"]',
fileIcon: '[data-testid="command-center-file-icon"]',
fileLine: '[data-testid="command-center-file-line"]',
} as const;
function parseSessionMessage(message: WebSocketMessage): Record<string, unknown> | null {
const raw = typeof message === "string" ? message : message.toString("utf8");
try {
const envelope = JSON.parse(raw) as { type?: unknown; message?: unknown };
return envelope.type === "session" && envelope.message && typeof envelope.message === "object"
? (envelope.message as Record<string, unknown>)
: null;
} catch {
return null;
}
}
export async function delayDirectorySuggestionResponses(page: Page, delayMs = 400): Promise<void> {
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
const server = ws.connectToServer();
ws.onMessage((message) => server.send(message));
server.onMessage((message) => {
const sessionMessage = parseSessionMessage(message);
if (sessionMessage?.type === "directory_suggestions_response") {
setTimeout(() => ws.send(message), delayMs);
return;
}
ws.send(message);
});
});
}
export async function failDirectorySuggestionRequests(page: Page): Promise<void> {
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
const server = ws.connectToServer();
ws.onMessage((message) => {
const sessionMessage = parseSessionMessage(message);
if (sessionMessage?.type === "directory_suggestions_request") {
const requestId = sessionMessage.requestId;
if (typeof requestId === "string") {
ws.send(
JSON.stringify({
type: "session",
message: {
type: "rpc_error",
payload: {
requestId,
requestType: "directory_suggestions_request",
error: "Test file search transport failure.",
code: "transport",
},
},
}),
);
}
return;
}
server.send(message);
});
server.onMessage((message) => ws.send(message));
});
}
export async function startCommandCenterLayoutObservation(page: Page): Promise<void> {
await page.evaluate((selectors) => {
const oracleWindow = window as LayoutOracleWindow;
const observation: LayoutObservation = { active: true, layoutShift: 0, samples: {} };
oracleWindow.__commandCenterLayoutObservation = observation;
const observedElements = new WeakSet<Element>();
const resizeObserver = new ResizeObserver(record);
const mutationObserver = new MutationObserver(discover);
function record(): void {
if (!observation.active) return;
for (const [name, selector] of Object.entries(selectors)) {
const element = document.querySelector<HTMLElement>(selector);
if (!element) continue;
const rect = element.getBoundingClientRect();
(observation.samples[name] ??= []).push({
height: rect.height,
width: rect.width,
x: rect.x,
y: rect.y,
});
}
}
function discover(): void {
for (const selector of Object.values(selectors)) {
const element = document.querySelector(selector);
if (!element || observedElements.has(element)) continue;
observedElements.add(element);
resizeObserver.observe(element);
}
record();
}
mutationObserver.observe(document.body, { attributes: true, childList: true, subtree: true });
discover();
const sampleFrame = () => {
if (!observation.active) {
mutationObserver.disconnect();
resizeObserver.disconnect();
return;
}
record();
requestAnimationFrame(sampleFrame);
};
requestAnimationFrame(sampleFrame);
if (typeof PerformanceObserver !== "undefined") {
try {
const performanceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
observation.layoutShift += (entry as PerformanceEntry & { value?: number }).value ?? 0;
}
});
performanceObserver.observe({ type: "layout-shift", buffered: false });
} catch {
// LayoutShift is diagnostic-only on engines that do not expose it.
}
}
}, SURFACE_SELECTORS);
}
export async function expectStableCommandCenterLayout(page: Page): Promise<void> {
await page.waitForTimeout(500);
const observation = await page.evaluate(() => {
const state = (window as LayoutOracleWindow).__commandCenterLayoutObservation;
if (!state) throw new Error("Command center layout observer was not installed");
state.active = false;
return state;
});
for (const surface of Object.keys(SURFACE_SELECTORS)) {
const samples = observation.samples[surface] ?? [];
expect(samples.length, `${surface}: ${JSON.stringify(observation)}`).toBeGreaterThan(0);
for (const dimension of ["height", "width", "x", "y"] as const) {
const values = samples.map((sample) => sample[dimension]);
expect(
Math.max(...values) - Math.min(...values),
`${surface}.${dimension}: ${JSON.stringify(samples)}`,
).toBeLessThanOrEqual(1);
}
}
expect(observation.layoutShift, JSON.stringify(observation)).toBeLessThanOrEqual(0.001);
}
export async function expectOneLineTruncatedFileResult(
row: Locator,
input: { filename: string; path: string },
): Promise<void> {
await expect(row).toContainText(`${input.filename} ${input.path}`);
await expect(row.getByTestId("command-center-file-icon")).toBeVisible();
const line = row.getByTestId("command-center-file-line");
const filename = row.getByTestId("command-center-file-name");
const filePath = row.getByTestId("command-center-file-path");
const geometry = await Promise.all([
row.boundingBox(),
line.boundingBox(),
filename.boundingBox(),
filePath.boundingBox(),
line.evaluate((element) => ({
clientWidth: element.clientWidth,
overflow: getComputedStyle(element).overflow,
scrollWidth: element.scrollWidth,
whiteSpace: getComputedStyle(element).whiteSpace,
})),
]);
const [rowBox, lineBox, filenameBox, pathBox, overflow] = geometry;
expect(rowBox).not.toBeNull();
expect(lineBox).not.toBeNull();
expect(filenameBox).not.toBeNull();
expect(pathBox).not.toBeNull();
expect(rowBox?.height).toBe(36);
expect(Math.abs((filenameBox?.y ?? 0) - (pathBox?.y ?? 0))).toBeLessThanOrEqual(1);
expect(overflow.scrollWidth).toBeGreaterThan(overflow.clientWidth);
expect(overflow.overflow).toBe("hidden");
expect(overflow.whiteSpace).toBe("nowrap");
}
@@ -13,7 +13,7 @@ import {
} from "react-native"; } from "react-native";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Check, ChevronRight, Folder } from "lucide-react-native"; import { Check, ChevronRight, Folder, X } from "lucide-react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { import {
BottomSheetBackdrop, BottomSheetBackdrop,
@@ -22,6 +22,8 @@ import {
type BottomSheetFlatListMethods, type BottomSheetFlatListMethods,
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import { AgentStatusDot } from "@/components/agent-status-dot"; import { AgentStatusDot } from "@/components/agent-status-dot";
import { MaterialFileIcon } from "@/components/material-file-icon";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Shortcut } from "@/components/ui/shortcut"; import { Shortcut } from "@/components/ui/shortcut";
import { import {
IsolatedBottomSheetModal, IsolatedBottomSheetModal,
@@ -37,7 +39,10 @@ import {
useWebOverlayRegistration, useWebOverlayRegistration,
} from "@/lib/overlay-root"; } from "@/lib/overlay-root";
import { useHosts } from "@/runtime/host-runtime"; import { useHosts } from "@/runtime/host-runtime";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import {
useKeyboardShortcutsStore,
type CommandCenterScope,
} from "@/stores/keyboard-shortcuts-store";
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher"; import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
import { import {
@@ -56,11 +61,13 @@ import {
preserveActiveResultId, preserveActiveResultId,
projectCommandCenterRows, projectCommandCenterRows,
type CommandCenterAgentResult, type CommandCenterAgentResult,
type CommandCenterFileResult,
type CommandCenterListRow, type CommandCenterListRow,
type CommandCenterResult, type CommandCenterResult,
type CommandCenterResultSection, type CommandCenterResultSection,
type CommandCenterWorkspaceResult, type CommandCenterWorkspaceResult,
} from "./results"; } from "./results";
import { useWorkspaceFileSearch } from "./workspace-file-search";
const ThemedBottomSheetTextInput = withUnistyles(BottomSheetTextInput, (theme) => ({ const ThemedBottomSheetTextInput = withUnistyles(BottomSheetTextInput, (theme) => ({
placeholderTextColor: theme.colors.foregroundMuted, placeholderTextColor: theme.colors.foregroundMuted,
@@ -73,6 +80,10 @@ const ThemedCheck = withUnistyles(Check, (theme) => ({ color: theme.colors.foreg
const ThemedChevronRight = withUnistyles(ChevronRight, (theme) => ({ const ThemedChevronRight = withUnistyles(ChevronRight, (theme) => ({
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
})); }));
const ThemedX = withUnistyles(X, (theme) => ({ color: theme.colors.foregroundMuted }));
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner, (theme) => ({
color: theme.colors.foregroundMuted,
}));
const COMMAND_CENTER_SNAP_POINTS = ["60%", "90%"]; const COMMAND_CENTER_SNAP_POINTS = ["60%", "90%"];
const KEYBOARD_SHOULD_PERSIST_TAPS = "always" as const; const KEYBOARD_SHOULD_PERSIST_TAPS = "always" as const;
const DEFAULT_CATEGORY_RESULT_LIMIT = 5; const DEFAULT_CATEGORY_RESULT_LIMIT = 5;
@@ -191,6 +202,8 @@ function useBuiltInSections(open: boolean, query: string): CommandCenterResultSe
interface CommandCenterState { interface CommandCenterState {
open: boolean; open: boolean;
scope: CommandCenterScope;
clearScope(): void;
query: string; query: string;
setQuery(query: string): void; setQuery(query: string): void;
activeId: string | null; activeId: string | null;
@@ -199,27 +212,58 @@ interface CommandCenterState {
rowIndexByResultId: ReadonlyMap<string, number>; rowIndexByResultId: ReadonlyMap<string, number>;
offsets: readonly number[]; offsets: readonly number[];
inputRef: React.RefObject<TextInput | null>; inputRef: React.RefObject<TextInput | null>;
fileSearchLoading: boolean;
fileSearchError: string | null;
close(): void; close(): void;
select(result: CommandCenterResult): void; select(result: CommandCenterResult): void;
key(key: string): boolean; key(key: string): boolean;
} }
function useCommandCenterState(): CommandCenterState { function useCommandCenterState(): CommandCenterState {
const { t } = useTranslation();
const open = useKeyboardShortcutsStore((state) => state.commandCenterOpen); const open = useKeyboardShortcutsStore((state) => state.commandCenterOpen);
const scope = useKeyboardShortcutsStore((state) => state.commandCenterScope);
const setOpen = useKeyboardShortcutsStore((state) => state.setCommandCenterOpen); const setOpen = useKeyboardShortcutsStore((state) => state.setCommandCenterOpen);
const setScope = useKeyboardShortcutsStore((state) => state.setCommandCenterScope);
const snapshot = useCommandCenterContributions(); const snapshot = useCommandCenterContributions();
const inputRef = useRef<TextInput>(null); const inputRef = useRef<TextInput>(null);
const previousOpenRef = useRef(open); const previousOpenRef = useRef(open);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [activeId, setActiveId] = useState<string | null>(null); const [activeId, setActiveId] = useState<string | null>(null);
const builtInSections = useBuiltInSections(open, query); const builtInSections = useBuiltInSections(open, query);
const {
entries: fileSearchEntries,
loading: fileSearchLoading,
error: fileSearchError,
openFile,
} = useWorkspaceFileSearch({
enabled: open && (scope === "files" || Boolean(query.trim())),
query,
});
const fileSections = useMemo<CommandCenterResultSection[]>(() => {
const results = fileSearchEntries.map<CommandCenterFileResult>((entry) => ({
kind: "file",
id: `file:${entry.path}`,
filePath: entry.path,
title: entry.name,
subtitle: entry.directory,
searchText: entry.path.toLowerCase(),
run: () => openFile(entry.path),
}));
return [{ id: "files", rank: 4, title: t("shell.commandCenter.files"), results }];
}, [fileSearchEntries, openFile, t]);
const contributionSections = useMemo( const contributionSections = useMemo(
() => buildContributionSections(snapshot.contributions, query), () => buildContributionSections(snapshot.contributions, query),
[query, snapshot.contributions], [query, snapshot.contributions],
); );
const projection = useMemo( const projection = useMemo(
() => projectCommandCenterRows([...contributionSections, ...builtInSections]), () =>
[builtInSections, contributionSections], projectCommandCenterRows(
scope === "files"
? fileSections
: [...contributionSections, ...fileSections, ...builtInSections],
),
[builtInSections, contributionSections, fileSections, scope],
); );
const resolvedActiveId = preserveActiveResultId(activeId, projection.selectableResults); const resolvedActiveId = preserveActiveResultId(activeId, projection.selectableResults);
@@ -239,6 +283,10 @@ function useCommandCenterState(): CommandCenterState {
close(); close();
return true; return true;
} }
if (pressed === "Backspace" && !query.trim() && scope) {
setScope(null);
return true;
}
if (pressed === "Enter") { if (pressed === "Enter") {
const selected = results.find((result) => result.id === resolvedActiveId); const selected = results.find((result) => result.id === resolvedActiveId);
if (!selected) return false; if (!selected) return false;
@@ -251,7 +299,7 @@ function useCommandCenterState(): CommandCenterState {
setActiveId(moveActiveResultId(resolvedActiveId, results, direction)); setActiveId(moveActiveResultId(resolvedActiveId, results, direction));
return true; return true;
}, },
[close, open, projection.selectableResults, resolvedActiveId, select], [close, open, projection.selectableResults, query, resolvedActiveId, scope, select, setScope],
); );
useEffect(() => { useEffect(() => {
@@ -277,6 +325,8 @@ function useCommandCenterState(): CommandCenterState {
return { return {
open, open,
scope,
clearScope: () => setScope(null),
query, query,
setQuery, setQuery,
activeId: resolvedActiveId, activeId: resolvedActiveId,
@@ -285,6 +335,8 @@ function useCommandCenterState(): CommandCenterState {
rowIndexByResultId: projection.rowIndexByResultId, rowIndexByResultId: projection.rowIndexByResultId,
offsets: projection.offsets, offsets: projection.offsets,
inputRef, inputRef,
fileSearchLoading,
fileSearchError,
close, close,
select, select,
key, key,
@@ -303,7 +355,10 @@ const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRo
result.kind === "contribution" && result.contribution.presentation.kind === "choice" result.kind === "contribution" && result.contribution.presentation.kind === "choice"
? result.contribution.presentation ? result.contribution.presentation
: null; : null;
const accessibilityLabel = choice?.path.join(" › "); const accessibilityLabel =
result.kind === "file"
? [result.title, result.subtitle].filter(Boolean).join(" ")
: choice?.path.join(" › ");
const accessibilityState = useMemo( const accessibilityState = useMemo(
() => (isNative && choice ? { selected: choice.selected } : undefined), () => (isNative && choice ? { selected: choice.selected } : undefined),
[choice], [choice],
@@ -329,7 +384,9 @@ const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRo
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
accessibilityState={accessibilityState} accessibilityState={accessibilityState}
aria-pressed={isWeb ? choice?.selected : undefined} aria-pressed={isWeb ? choice?.selected : undefined}
testID={choice?.testId} testID={
result.kind === "file" ? `command-center-file-row-${result.filePath}` : choice?.testId
}
> >
<ResultContent result={result} /> <ResultContent result={result} />
</Pressable> </Pressable>
@@ -337,6 +394,28 @@ const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRo
}); });
function ResultContent({ result }: { result: CommandCenterResult }) { function ResultContent({ result }: { result: CommandCenterResult }) {
if (result.kind === "file") {
return (
<View style={styles.rowContent}>
<View style={styles.rowMain}>
<View style={styles.iconSlot} testID="command-center-file-icon">
<MaterialFileIcon fileName={result.title} size={16} />
</View>
<Text style={styles.fileLine} numberOfLines={1} testID="command-center-file-line">
<Text style={styles.fileName} testID="command-center-file-name">
{result.title}
</Text>
{result.subtitle ? (
<Text style={styles.filePath} testID="command-center-file-path">
{" "}
{result.subtitle}
</Text>
) : null}
</Text>
</View>
</View>
);
}
if (result.kind === "agent") { if (result.kind === "agent") {
const agent = result.agent; const agent = result.agent;
return ( return (
@@ -545,8 +624,24 @@ export function CommandCenter() {
); );
const keyExtractor = useCallback((row: CommandCenterListRow) => row.key, []); const keyExtractor = useCallback((row: CommandCenterListRow) => row.key, []);
const empty = useMemo( const empty = useMemo(
() => <Text style={styles.emptyText}>{t("shell.commandCenter.noMatches")}</Text>, () =>
[t], state.fileSearchError || state.fileSearchLoading ? null : (
<Text style={styles.emptyText}>{t("shell.commandCenter.noMatches")}</Text>
),
[state.fileSearchError, state.fileSearchLoading, t],
);
const fileSearchError = useMemo(
() =>
state.fileSearchError ? (
<Text
accessibilityLiveRegion="polite"
style={styles.errorText}
testID="command-center-file-search-error"
>
{t("common.errors.error")}: {state.fileSearchError}
</Text>
) : null,
[state.fileSearchError, t],
); );
const commonListProps = { const commonListProps = {
data: state.rows, data: state.rows,
@@ -554,6 +649,8 @@ export function CommandCenter() {
keyExtractor, keyExtractor,
getItemLayout, getItemLayout,
ListEmptyComponent: empty, ListEmptyComponent: empty,
style: styles.results,
testID: "command-center-results",
keyboardShouldPersistTaps: KEYBOARD_SHOULD_PERSIST_TAPS, keyboardShouldPersistTaps: KEYBOARD_SHOULD_PERSIST_TAPS,
showsVerticalScrollIndicator: false, showsVerticalScrollIndicator: false,
initialNumToRender: 12, initialNumToRender: 12,
@@ -606,7 +703,10 @@ export function CommandCenter() {
keyboardBlurBehavior="restore" keyboardBlurBehavior="restore"
accessible={false} accessible={false}
> >
<View style={styles.bottomSheetHeader}> <View style={[styles.bottomSheetHeader, styles.searchRow]} testID="command-center-header">
{state.scope === "files" ? (
<ScopeChip label={t("shell.commandCenter.files")} onRemove={state.clearScope} />
) : null}
<ThemedBottomSheetTextInput <ThemedBottomSheetTextInput
testID="command-center-input" testID="command-center-input"
ref={bottomSheetInputRef} ref={bottomSheetInputRef}
@@ -614,13 +714,22 @@ export function CommandCenter() {
onChangeText={state.setQuery} onChangeText={state.setQuery}
onKeyPress={keyPress} onKeyPress={keyPress}
onSubmitEditing={submit} onSubmitEditing={submit}
placeholder={t("shell.commandCenter.placeholder")} placeholder={
style={styles.input} state.scope === "files"
? t("shell.commandCenter.filePlaceholder")
: t("shell.commandCenter.placeholder")
}
style={[styles.input, styles.growingInput]}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
autoFocus autoFocus
/> />
<FileSearchLoadingIndicator
loading={state.fileSearchLoading}
label={t("shell.commandCenter.searchingFiles")}
/>
</View> </View>
{fileSearchError}
<BottomSheetFlatList ref={bottomSheetListRef} {...commonListProps} /> <BottomSheetFlatList ref={bottomSheetListRef} {...commonListProps} />
</IsolatedBottomSheetModal> </IsolatedBottomSheetModal>
); );
@@ -632,20 +741,32 @@ export function CommandCenter() {
<View style={styles.overlay}> <View style={styles.overlay}>
<Pressable style={styles.backdrop} onPress={state.close} /> <Pressable style={styles.backdrop} onPress={state.close} />
<View ref={setWebOverlayScope} testID="command-center-panel" style={styles.panel}> <View ref={setWebOverlayScope} testID="command-center-panel" style={styles.panel}>
<View style={styles.header}> <View style={[styles.header, styles.searchRow]} testID="command-center-header">
{state.scope === "files" ? (
<ScopeChip label={t("shell.commandCenter.files")} onRemove={state.clearScope} />
) : null}
<ThemedTextInput <ThemedTextInput
testID="command-center-input" testID="command-center-input"
ref={state.inputRef} ref={state.inputRef}
value={state.query} value={state.query}
onChangeText={state.setQuery} onChangeText={state.setQuery}
placeholder={t("shell.commandCenter.placeholder")} placeholder={
style={styles.input} state.scope === "files"
? t("shell.commandCenter.filePlaceholder")
: t("shell.commandCenter.placeholder")
}
style={[styles.input, styles.growingInput]}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
autoFocus autoFocus
/> />
<FileSearchLoadingIndicator
loading={state.fileSearchLoading}
label={t("shell.commandCenter.searchingFiles")}
/>
</View> </View>
<FlatList ref={listRef} style={styles.results} {...commonListProps} /> {fileSearchError}
<FlatList ref={listRef} {...commonListProps} />
</View> </View>
</View> </View>
</Modal> </Modal>
@@ -653,6 +774,41 @@ export function CommandCenter() {
); );
} }
function FileSearchLoadingIndicator({ loading, label }: { loading: boolean; label: string }) {
return (
<View style={styles.fileSearchStatus} testID="command-center-file-search-status">
{loading ? (
<View
accessibilityLabel={label}
accessibilityRole="progressbar"
testID="command-center-file-search-loading"
>
<ThemedLoadingSpinner size={14} />
</View>
) : null}
</View>
);
}
// The chip sits at the input's own height and type size so it reads as the scope of the field
// rather than a badge dropped into the query. Its 28px matches the input's line box, so showing or
// dropping the scope cannot resize the header.
function ScopeChip({ label, onRemove }: { label: string; onRemove(): void }) {
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={label}
onPress={onRemove}
style={styles.scopeChip}
testID="command-center-files-scope"
>
<ThemedFolder size={14} strokeWidth={2.2} />
<Text style={styles.scopeChipLabel}>{label}</Text>
<ThemedX size={12} strokeWidth={2.2} />
</Pressable>
);
}
const styles = StyleSheet.create((theme) => ({ const styles = StyleSheet.create((theme) => ({
overlay: { overlay: {
flex: 1, flex: 1,
@@ -663,6 +819,7 @@ const styles = StyleSheet.create((theme) => ({
backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)" }, backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)" },
panel: { panel: {
width: 640, width: 640,
height: 560,
maxWidth: "92%", maxWidth: "92%",
maxHeight: "80%", maxHeight: "80%",
borderWidth: 1, borderWidth: 1,
@@ -690,7 +847,29 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground, color: theme.colors.foreground,
outlineWidth: 0, outlineWidth: 0,
}, },
results: { flexGrow: 0 }, growingInput: { flex: 1, minWidth: 0 },
searchRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2] },
fileSearchStatus: { width: 14, height: 14, alignItems: "center", justifyContent: "center" },
scopeChip: {
// Stretching to the row rather than setting a height keeps the chip exactly as tall as the
// input's line box, so showing or dropping the scope can never resize the header.
alignSelf: "stretch",
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1.5],
marginRight: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.md,
borderWidth: 1,
borderColor: theme.colors.borderAccent,
backgroundColor: theme.colors.surface2,
},
scopeChipLabel: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: "500",
},
results: { flex: 1 },
sectionLabel: { sectionLabel: {
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[4],
paddingBottom: theme.spacing[2], paddingBottom: theme.spacing[2],
@@ -726,6 +905,9 @@ const styles = StyleSheet.create((theme) => ({
lineHeight: 18, lineHeight: 18,
flexShrink: 1, flexShrink: 1,
}, },
fileLine: { flex: 1, minWidth: 0, fontSize: theme.fontSize.sm, lineHeight: 20 },
fileName: { color: theme.colors.foreground },
filePath: { color: theme.colors.foregroundMuted },
subtitle: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, lineHeight: 16 }, subtitle: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, lineHeight: 16 },
iconSlot: { width: 16, height: 20, alignItems: "center", justifyContent: "center" }, iconSlot: { width: 16, height: 20, alignItems: "center", justifyContent: "center" },
rowShortcut: { flexShrink: 0 }, rowShortcut: { flexShrink: 0 },
@@ -755,6 +937,14 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
}, },
errorText: {
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
color: theme.colors.statusDanger,
fontSize: theme.fontSize.sm,
},
sheetBackground: { backgroundColor: theme.colors.surface0 }, sheetBackground: { backgroundColor: theme.colors.surface0 },
sheetHandle: { backgroundColor: theme.colors.palette.zinc[600] }, sheetHandle: { backgroundColor: theme.colors.palette.zinc[600] },
titledSection: { height: 32, justifyContent: "flex-end" }, titledSection: { height: 32, justifyContent: "flex-end" },
@@ -20,6 +20,16 @@ export interface CommandCenterAgentResult {
run(): void; run(): void;
} }
export interface CommandCenterFileResult {
kind: "file";
id: string;
filePath: string;
title: string;
subtitle: string;
searchText: string;
run(): void;
}
export interface CommandCenterContributionResult { export interface CommandCenterContributionResult {
kind: "contribution"; kind: "contribution";
id: string; id: string;
@@ -31,6 +41,7 @@ export interface CommandCenterContributionResult {
export type CommandCenterResult = export type CommandCenterResult =
| CommandCenterWorkspaceResult | CommandCenterWorkspaceResult
| CommandCenterAgentResult | CommandCenterAgentResult
| CommandCenterFileResult
| CommandCenterContributionResult; | CommandCenterContributionResult;
export interface CommandCenterResultSection { export interface CommandCenterResultSection {
@@ -78,6 +89,7 @@ function contributionSearchText(contribution: CommandCenterContribution): string
function resultHeight(result: CommandCenterResult): number { function resultHeight(result: CommandCenterResult): number {
if (result.kind === "workspace" || result.kind === "agent") return 56; if (result.kind === "workspace" || result.kind === "agent") return 56;
if (result.kind === "file") return 36;
if (result.contribution.presentation.kind === "action") { if (result.contribution.presentation.kind === "action") {
return result.contribution.presentation.subtitle ? 56 : 36; return result.contribution.presentation.subtitle ? 56 : 36;
} }
@@ -0,0 +1,15 @@
export interface WorkspaceFileSearchEntry {
path: string;
name: string;
directory: string;
}
export function describeWorkspaceFilePath(path: string): WorkspaceFileSearchEntry {
const normalized = path.replace(/\\/g, "/");
const separator = normalized.lastIndexOf("/");
return {
path: normalized,
name: separator >= 0 ? normalized.slice(separator + 1) : normalized,
directory: separator >= 0 ? normalized.slice(0, separator) : "",
};
}
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { describeWorkspaceFilePath } from "./workspace-file-search-model";
describe("describeWorkspaceFilePath", () => {
it("separates a workspace-relative file into its row labels", () => {
expect(describeWorkspaceFilePath("src/components/message.tsx")).toEqual({
path: "src/components/message.tsx",
name: "message.tsx",
directory: "src/components",
});
});
it("normalizes Windows separators before opening or presenting a file", () => {
expect(describeWorkspaceFilePath("src\\components\\message.tsx")).toEqual({
path: "src/components/message.tsx",
name: "message.tsx",
directory: "src/components",
});
});
it("keeps root files free of a redundant directory label", () => {
expect(describeWorkspaceFilePath("package.json")).toEqual({
path: "package.json",
name: "package.json",
directory: "",
});
});
});
@@ -0,0 +1,148 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { openWorkspaceFileFromExplorer } from "@/screens/workspace/workspace-file-open-command";
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import { usePanelStore } from "@/stores/panel-store";
import { useSessionStore } from "@/stores/session-store";
import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
import { clearCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
import {
describeWorkspaceFilePath,
type WorkspaceFileSearchEntry,
} from "./workspace-file-search-model";
interface DirectorySuggestionEntry {
path: string;
}
const FILE_SEARCH_DEBOUNCE_MS = 100;
const FILE_SEARCH_LIMIT = 100;
interface WorkspaceFileSearchState {
sourceKey: string | null;
requestKey: string | null;
entries: readonly WorkspaceFileSearchEntry[];
loading: boolean;
error: string | null;
}
const EMPTY_STATE: WorkspaceFileSearchState = {
sourceKey: null,
requestKey: null,
entries: [],
loading: false,
error: null,
};
function errorMessage(error: unknown): string {
if (!(error instanceof Error)) return String(error);
if (error.name !== "DaemonRpcError") return error.message;
return error.message.replace(/ requestType=\S+(?: code=\S+)?$/, "");
}
function describeFileEntries(
entries: readonly DirectorySuggestionEntry[],
): WorkspaceFileSearchEntry[] {
return entries.map(({ path }) => describeWorkspaceFilePath(path));
}
export function useWorkspaceFileSearch(input: { enabled: boolean; query: string }): {
entries: readonly WorkspaceFileSearchEntry[];
loading: boolean;
error: string | null;
openFile(path: string): void;
} {
const selection = useActiveWorkspaceSelection();
const serverId = selection?.serverId ?? null;
const workspaceId = selection?.workspaceId ?? null;
const cwd = useWorkspaceDirectory(serverId, workspaceId);
const client = useSessionStore((state) =>
serverId ? (state.sessions[serverId]?.client ?? null) : null,
);
const [state, setState] = useState<WorkspaceFileSearchState>(EMPTY_STATE);
const sourceKey = useMemo(
() => (serverId && workspaceId && cwd && client ? `${serverId}\0${workspaceId}\0${cwd}` : null),
[client, cwd, serverId, workspaceId],
);
const requestKey = useMemo(
() => (input.enabled && sourceKey ? `${sourceKey}\0${input.query}` : null),
[input.enabled, input.query, sourceKey],
);
useEffect(() => {
if (!requestKey || !client || !cwd) {
setState(EMPTY_STATE);
return;
}
const activeClient = client;
const activeCwd = cwd;
let cancelled = false;
setState((previous) => ({
sourceKey,
requestKey,
entries: previous.sourceKey === sourceKey ? previous.entries : [],
loading: true,
error: null,
}));
async function search(): Promise<void> {
try {
const payload = await activeClient.getDirectorySuggestions({
cwd: activeCwd,
query: input.query,
includeFiles: true,
includeDirectories: false,
limit: FILE_SEARCH_LIMIT,
});
if (cancelled) return;
setState({
sourceKey,
requestKey,
entries: payload.error ? [] : describeFileEntries(payload.entries),
loading: false,
error: payload.error ?? null,
});
} catch (error) {
if (!cancelled) {
setState({
sourceKey,
requestKey,
entries: [],
loading: false,
error: errorMessage(error),
});
}
}
}
const timer = setTimeout(() => void search(), input.query.trim() ? FILE_SEARCH_DEBOUNCE_MS : 0);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [client, cwd, input.query, requestKey, sourceKey]);
const openFile = useCallback(
(path: string) => {
if (!serverId || !workspaceId) return;
clearCommandCenterFocusRestoreElement();
openWorkspaceFileFromExplorer({
filePath: path,
persistenceKey: buildWorkspaceTabPersistenceKey({ serverId, workspaceId }),
showMobileAgent: usePanelStore.getState().showMobileAgent,
openWorkspaceTabFocused: useWorkspaceLayoutStore.getState().openTabFocused,
focusWorkspaceTab: useWorkspaceLayoutStore.getState().focusTab,
});
},
[serverId, workspaceId],
);
return {
entries: requestKey && state.sourceKey === sourceKey ? state.entries : [],
loading: Boolean(requestKey) && (state.requestKey !== requestKey || state.loading),
error: state.requestKey === requestKey ? state.error : null,
openFile,
};
}
@@ -201,7 +201,9 @@ export function useKeyboardShortcuts({
setCommandCenterFocusRestoreElement(browserFocusRestoreElement); setCommandCenterFocusRestoreElement(browserFocusRestoreElement);
} }
} }
useKeyboardShortcutsStore.getState().setCommandCenterOpen(action.nextOpen); useKeyboardShortcutsStore
.getState()
.setCommandCenterOpen(action.nextOpen, action.scope ?? null);
return true; return true;
} }
case "shortcuts-dialog-toggle": case "shortcuts-dialog-toggle":
+5 -1
View File
@@ -190,7 +190,11 @@ describe("translation resources", () => {
expect(en.shell.menu.toggleSidebar).toBe("Toggle sidebar"); expect(en.shell.menu.toggleSidebar).toBe("Toggle sidebar");
expect(en.shell.menu.open).toBe("Open menu"); expect(en.shell.menu.open).toBe("Open menu");
expect(en.shell.menu.close).toBe("Close menu"); expect(en.shell.menu.close).toBe("Close menu");
expect(en.shell.commandCenter.placeholder).toBe("Search commands, workspaces, and agents..."); expect(en.shell.commandCenter.placeholder).toBe(
"Search commands, files, workspaces, and agents...",
);
expect(en.shell.commandCenter.filePlaceholder).toBe("Search files...");
expect(en.shell.commandCenter.files).toBe("Files");
expect(en.shell.commandCenter.noMatches).toBe("No matches"); expect(en.shell.commandCenter.noMatches).toBe("No matches");
expect(en.shell.commandCenter.actions).toBe("Actions"); expect(en.shell.commandCenter.actions).toBe("Actions");
expect(en.shell.commandCenter.workspaces).toBe("Workspaces"); expect(en.shell.commandCenter.workspaces).toBe("Workspaces");
+8 -4
View File
@@ -54,9 +54,12 @@ export const ar: TranslationResources = {
close: "إغلاق القائمة", close: "إغلاق القائمة",
}, },
commandCenter: { commandCenter: {
placeholder: "ابحث في الأوامر ومساحات العمل والوكلاء...", placeholder: "ابحث في الأوامر والملفات ومساحات العمل والوكلاء...",
filePlaceholder: "البحث في الملفات...",
searchingFiles: "جارٍ البحث في الملفات...",
noMatches: "لا توجد مباريات", noMatches: "لا توجد مباريات",
actions: "الإجراءات", actions: "الإجراءات",
files: "الملفات",
workspaces: "مساحات العمل", workspaces: "مساحات العمل",
agents: "الوكلاء", agents: "الوكلاء",
newAgent: "وكيل جديد", newAgent: "وكيل جديد",
@@ -1912,10 +1915,10 @@ export const ar: TranslationResources = {
resetAll: "إعادة ضبط الكل", resetAll: "إعادة ضبط الكل",
}, },
sections: { sections: {
navigation: "ملاحة", general: "عام",
workspaces: "المشاريع ومساحات العمل",
tabsPanes: "علامات التبويب والأجزاء", tabsPanes: "علامات التبويب والأجزاء",
projects: "المشاريع", layout: "التخطيط",
panels: "لوحات",
agentInput: "إدخال Agent", agentInput: "إدخال Agent",
}, },
help: { help: {
@@ -1943,6 +1946,7 @@ export const ar: TranslationResources = {
moveTabDown: "حرك علامة التبويب لأسفل", moveTabDown: "حرك علامة التبويب لأسفل",
closePane: "إغلاق الجزء", closePane: "إغلاق الجزء",
newTerminal: "محطة جديدة", newTerminal: "محطة جديدة",
searchFiles: "البحث في الملفات",
toggleCommandCenter: "تبديل مركز القيادة", toggleCommandCenter: "تبديل مركز القيادة",
showKeyboardShortcuts: "إظهار اختصارات لوحة المفاتيح", showKeyboardShortcuts: "إظهار اختصارات لوحة المفاتيح",
toggleLeftSidebar: "تبديل الشريط الجانبي الأيسر", toggleLeftSidebar: "تبديل الشريط الجانبي الأيسر",
+8 -4
View File
@@ -52,9 +52,12 @@ export const en = {
close: "Close menu", close: "Close menu",
}, },
commandCenter: { commandCenter: {
placeholder: "Search commands, workspaces, and agents...", placeholder: "Search commands, files, workspaces, and agents...",
filePlaceholder: "Search files...",
searchingFiles: "Searching files...",
noMatches: "No matches", noMatches: "No matches",
actions: "Actions", actions: "Actions",
files: "Files",
workspaces: "Workspaces", workspaces: "Workspaces",
agents: "Agents", agents: "Agents",
newAgent: "New agent", newAgent: "New agent",
@@ -1922,10 +1925,10 @@ export const en = {
resetAll: "Reset all", resetAll: "Reset all",
}, },
sections: { sections: {
navigation: "Navigation", general: "General",
workspaces: "Projects & Workspaces",
tabsPanes: "Tabs & Panes", tabsPanes: "Tabs & Panes",
projects: "Projects", layout: "Layout",
panels: "Panels",
agentInput: "Agent Input", agentInput: "Agent Input",
}, },
help: { help: {
@@ -1953,6 +1956,7 @@ export const en = {
moveTabDown: "Move tab down", moveTabDown: "Move tab down",
closePane: "Close pane", closePane: "Close pane",
newTerminal: "New terminal", newTerminal: "New terminal",
searchFiles: "Search files",
toggleCommandCenter: "Toggle command center", toggleCommandCenter: "Toggle command center",
showKeyboardShortcuts: "Show keyboard shortcuts", showKeyboardShortcuts: "Show keyboard shortcuts",
toggleLeftSidebar: "Toggle left sidebar", toggleLeftSidebar: "Toggle left sidebar",
+8 -4
View File
@@ -54,9 +54,12 @@ export const es: TranslationResources = {
close: "Cerrar menú", close: "Cerrar menú",
}, },
commandCenter: { commandCenter: {
placeholder: "Buscar comandos, espacios de trabajo y agentes...", placeholder: "Buscar comandos, archivos, espacios de trabajo y agentes...",
filePlaceholder: "Buscar archivos...",
searchingFiles: "Buscando archivos...",
noMatches: "No hay coincidencias", noMatches: "No hay coincidencias",
actions: "Comportamiento", actions: "Comportamiento",
files: "Archivos",
workspaces: "Espacios de trabajo", workspaces: "Espacios de trabajo",
agents: "Agentes", agents: "Agentes",
newAgent: "Nuevo agente", newAgent: "Nuevo agente",
@@ -1960,10 +1963,10 @@ export const es: TranslationResources = {
resetAll: "Restablecer todo", resetAll: "Restablecer todo",
}, },
sections: { sections: {
navigation: "Navegación", general: "General",
workspaces: "Proyectos y espacios de trabajo",
tabsPanes: "Pestañas y paneles", tabsPanes: "Pestañas y paneles",
projects: "Proyectos", layout: "Diseño",
panels: "Paneles",
agentInput: "EntradaAgent", agentInput: "EntradaAgent",
}, },
help: { help: {
@@ -1991,6 +1994,7 @@ export const es: TranslationResources = {
moveTabDown: "Mover pestaña hacia abajo", moveTabDown: "Mover pestaña hacia abajo",
closePane: "Cerrar panel", closePane: "Cerrar panel",
newTerminal: "Nueva terminal", newTerminal: "Nueva terminal",
searchFiles: "Buscar archivos",
toggleCommandCenter: "Alternar centro de comando", toggleCommandCenter: "Alternar centro de comando",
showKeyboardShortcuts: "Mostrar atajos de teclado", showKeyboardShortcuts: "Mostrar atajos de teclado",
toggleLeftSidebar: "Alternar barra lateral izquierda", toggleLeftSidebar: "Alternar barra lateral izquierda",
+8 -4
View File
@@ -55,9 +55,12 @@ export const fr: TranslationResources = {
close: "Fermer le menu", close: "Fermer le menu",
}, },
commandCenter: { commandCenter: {
placeholder: "Rechercher des commandes, espaces de travail et agents...", placeholder: "Rechercher des commandes, fichiers, espaces de travail et agents...",
filePlaceholder: "Rechercher des fichiers...",
searchingFiles: "Recherche de fichiers...",
noMatches: "Aucune correspondance", noMatches: "Aucune correspondance",
actions: "Actes", actions: "Actes",
files: "Fichiers",
workspaces: "Espaces de travail", workspaces: "Espaces de travail",
agents: "Agents", agents: "Agents",
newAgent: "Nouvel agent", newAgent: "Nouvel agent",
@@ -1963,10 +1966,10 @@ export const fr: TranslationResources = {
resetAll: "Tout réinitialiser", resetAll: "Tout réinitialiser",
}, },
sections: { sections: {
navigation: "Navigation", general: "Général",
workspaces: "Projets et espaces de travail",
tabsPanes: "Onglets et volets", tabsPanes: "Onglets et volets",
projects: "Projets", layout: "Disposition",
panels: "Panneaux",
agentInput: "EntréeAgent", agentInput: "EntréeAgent",
}, },
help: { help: {
@@ -1994,6 +1997,7 @@ export const fr: TranslationResources = {
moveTabDown: "Déplacer l'onglet vers le bas", moveTabDown: "Déplacer l'onglet vers le bas",
closePane: "Fermer le volet", closePane: "Fermer le volet",
newTerminal: "Nouvelle borne", newTerminal: "Nouvelle borne",
searchFiles: "Rechercher des fichiers",
toggleCommandCenter: "Basculer le centre de commande", toggleCommandCenter: "Basculer le centre de commande",
showKeyboardShortcuts: "Afficher les raccourcis clavier", showKeyboardShortcuts: "Afficher les raccourcis clavier",
toggleLeftSidebar: "Basculer la barre latérale gauche", toggleLeftSidebar: "Basculer la barre latérale gauche",
+8 -4
View File
@@ -54,9 +54,12 @@ export const ja: TranslationResources = {
close: "メニューを閉じる", close: "メニューを閉じる",
}, },
commandCenter: { commandCenter: {
placeholder: "コマンド、ワークスペース、エージェントを検索...", placeholder: "コマンド、ファイル、ワークスペース、エージェントを検索...",
filePlaceholder: "ファイルを検索...",
searchingFiles: "ファイルを検索中...",
noMatches: "一致なし", noMatches: "一致なし",
actions: "アクション", actions: "アクション",
files: "ファイル",
workspaces: "ワークスペース", workspaces: "ワークスペース",
agents: "エージェント", agents: "エージェント",
newAgent: "新しいエージェント", newAgent: "新しいエージェント",
@@ -1928,10 +1931,10 @@ export const ja: TranslationResources = {
resetAll: "すべてリセット", resetAll: "すべてリセット",
}, },
sections: { sections: {
navigation: "ナビゲーション", general: "一般",
workspaces: "プロジェクト&ワークスペース",
tabsPanes: "タブ&ペイン", tabsPanes: "タブ&ペイン",
projects: "プロジェクト", layout: "レイアウト",
panels: "パネル",
agentInput: "エージェント入力", agentInput: "エージェント入力",
}, },
help: { help: {
@@ -1959,6 +1962,7 @@ export const ja: TranslationResources = {
moveTabDown: "タブを下に移動", moveTabDown: "タブを下に移動",
closePane: "ペインを閉じる", closePane: "ペインを閉じる",
newTerminal: "新しいターミナル", newTerminal: "新しいターミナル",
searchFiles: "ファイルを検索",
toggleCommandCenter: "コマンドセンターを切り替え", toggleCommandCenter: "コマンドセンターを切り替え",
showKeyboardShortcuts: "キーボードショートカットを表示", showKeyboardShortcuts: "キーボードショートカットを表示",
toggleLeftSidebar: "左サイドバーを切り替え", toggleLeftSidebar: "左サイドバーを切り替え",
+8 -4
View File
@@ -54,9 +54,12 @@ export const ko: TranslationResources = {
close: "메뉴 닫기", close: "메뉴 닫기",
}, },
commandCenter: { commandCenter: {
placeholder: "명령, 워크스페이스 및 에이전트 검색...", placeholder: "명령, 파일, 워크스페이스 및 에이전트 검색...",
filePlaceholder: "파일 검색...",
searchingFiles: "파일 검색 중...",
noMatches: "일치 항목 없음", noMatches: "일치 항목 없음",
actions: "작업", actions: "작업",
files: "파일",
workspaces: "워크스페이스", workspaces: "워크스페이스",
agents: "에이전트", agents: "에이전트",
newAgent: "새 에이전트", newAgent: "새 에이전트",
@@ -1923,10 +1926,10 @@ export const ko: TranslationResources = {
resetAll: "모두 재설정", resetAll: "모두 재설정",
}, },
sections: { sections: {
navigation: "탐색", general: "일반",
workspaces: "프로젝트 및 워크스페이스",
tabsPanes: "탭 및 창", tabsPanes: "탭 및 창",
projects: "프로젝트", layout: "레이아웃",
panels: "패널",
agentInput: "에이전트 입력", agentInput: "에이전트 입력",
}, },
help: { help: {
@@ -1954,6 +1957,7 @@ export const ko: TranslationResources = {
moveTabDown: "탭을 아래로 이동", moveTabDown: "탭을 아래로 이동",
closePane: "창 닫기", closePane: "창 닫기",
newTerminal: "새 터미널", newTerminal: "새 터미널",
searchFiles: "파일 검색",
toggleCommandCenter: "명령 센터 토글", toggleCommandCenter: "명령 센터 토글",
showKeyboardShortcuts: "키보드 단축키 표시", showKeyboardShortcuts: "키보드 단축키 표시",
toggleLeftSidebar: "왼쪽 사이드바 토글", toggleLeftSidebar: "왼쪽 사이드바 토글",
+8 -4
View File
@@ -54,9 +54,12 @@ export const ptBR: TranslationResources = {
close: "Fechar menu", close: "Fechar menu",
}, },
commandCenter: { commandCenter: {
placeholder: "Buscar comandos, espaços de trabalho e agentes...", placeholder: "Buscar comandos, arquivos, espaços de trabalho e agentes...",
filePlaceholder: "Pesquisar arquivos...",
searchingFiles: "Pesquisando arquivos...",
noMatches: "Nenhuma correspondência", noMatches: "Nenhuma correspondência",
actions: "Ações", actions: "Ações",
files: "Arquivos",
workspaces: "Espaços de trabalho", workspaces: "Espaços de trabalho",
agents: "Agentes", agents: "Agentes",
newAgent: "Novo agente", newAgent: "Novo agente",
@@ -1943,10 +1946,10 @@ export const ptBR: TranslationResources = {
resetAll: "Redefinir tudo", resetAll: "Redefinir tudo",
}, },
sections: { sections: {
navigation: "Navegação", general: "Geral",
workspaces: "Projetos e workspaces",
tabsPanes: "Abas e painéis", tabsPanes: "Abas e painéis",
projects: "Projetos", layout: "Layout",
panels: "Painéis",
agentInput: "Entrada do agente", agentInput: "Entrada do agente",
}, },
help: { help: {
@@ -1974,6 +1977,7 @@ export const ptBR: TranslationResources = {
moveTabDown: "Mover aba para baixo", moveTabDown: "Mover aba para baixo",
closePane: "Fechar painel", closePane: "Fechar painel",
newTerminal: "Novo terminal", newTerminal: "Novo terminal",
searchFiles: "Pesquisar arquivos",
toggleCommandCenter: "Alternar central de comandos", toggleCommandCenter: "Alternar central de comandos",
showKeyboardShortcuts: "Mostrar atalhos de teclado", showKeyboardShortcuts: "Mostrar atalhos de teclado",
toggleLeftSidebar: "Alternar barra lateral esquerda", toggleLeftSidebar: "Alternar barra lateral esquerda",
+8 -4
View File
@@ -54,9 +54,12 @@ export const ru: TranslationResources = {
close: "Закрыть меню", close: "Закрыть меню",
}, },
commandCenter: { commandCenter: {
placeholder: "Поиск команд, рабочих пространств и агентов...", placeholder: "Поиск команд, файлов, рабочих пространств и агентов...",
filePlaceholder: "Поиск файлов...",
searchingFiles: "Поиск файлов...",
noMatches: "Нет совпадений", noMatches: "Нет совпадений",
actions: "Действия", actions: "Действия",
files: "Файлы",
workspaces: "Рабочие пространства", workspaces: "Рабочие пространства",
agents: "Агенты", agents: "Агенты",
newAgent: "Новый агент", newAgent: "Новый агент",
@@ -1950,10 +1953,10 @@ export const ru: TranslationResources = {
resetAll: "Сбросить все", resetAll: "Сбросить все",
}, },
sections: { sections: {
navigation: "Навигация", general: "Общие",
workspaces: "Проекты и рабочие пространства",
tabsPanes: "Вкладки и панели", tabsPanes: "Вкладки и панели",
projects: "Проекты", layout: "Макет",
panels: "Панели",
agentInput: "Вход Agent", agentInput: "Вход Agent",
}, },
help: { help: {
@@ -1981,6 +1984,7 @@ export const ru: TranslationResources = {
moveTabDown: "Переместить вкладку вниз", moveTabDown: "Переместить вкладку вниз",
closePane: "Закрыть панель", closePane: "Закрыть панель",
newTerminal: "Новый терминал", newTerminal: "Новый терминал",
searchFiles: "Поиск файлов",
toggleCommandCenter: "Переключить командный центр", toggleCommandCenter: "Переключить командный центр",
showKeyboardShortcuts: "Показать сочетания клавиш", showKeyboardShortcuts: "Показать сочетания клавиш",
toggleLeftSidebar: "Переключить левую боковую панель", toggleLeftSidebar: "Переключить левую боковую панель",
+8 -4
View File
@@ -54,9 +54,12 @@ export const zhCN: TranslationResources = {
close: "关闭菜单", close: "关闭菜单",
}, },
commandCenter: { commandCenter: {
placeholder: "搜索命令、工作区和 Agent...", placeholder: "搜索命令、文件、工作区和 Agent...",
filePlaceholder: "搜索文件...",
searchingFiles: "正在搜索文件...",
noMatches: "没有匹配项", noMatches: "没有匹配项",
actions: "操作", actions: "操作",
files: "文件",
workspaces: "工作区", workspaces: "工作区",
agents: "Agents", agents: "Agents",
newAgent: "新建 Agent", newAgent: "新建 Agent",
@@ -1890,10 +1893,10 @@ export const zhCN: TranslationResources = {
resetAll: "全部重置", resetAll: "全部重置",
}, },
sections: { sections: {
navigation: "导航", general: "通用",
workspaces: "项目和工作区",
tabsPanes: "标签和窗格", tabsPanes: "标签和窗格",
projects: "项目", layout: "布局",
panels: "面板",
agentInput: "Agent 输入", agentInput: "Agent 输入",
}, },
help: { help: {
@@ -1921,6 +1924,7 @@ export const zhCN: TranslationResources = {
moveTabDown: "向下移动标签", moveTabDown: "向下移动标签",
closePane: "关闭窗格", closePane: "关闭窗格",
newTerminal: "新建终端", newTerminal: "新建终端",
searchFiles: "搜索文件",
toggleCommandCenter: "切换命令中心", toggleCommandCenter: "切换命令中心",
showKeyboardShortcuts: "显示键盘快捷键", showKeyboardShortcuts: "显示键盘快捷键",
toggleLeftSidebar: "切换左侧边栏", toggleLeftSidebar: "切换左侧边栏",
+1
View File
@@ -41,6 +41,7 @@ export type KeyboardActionId =
| "sidebar.toggle.both" | "sidebar.toggle.both"
| "settings.toggle" | "settings.toggle"
| "command-center.toggle" | "command-center.toggle"
| "command-center.files"
| "shortcuts.dialog.toggle" | "shortcuts.dialog.toggle"
| "workspace.terminal.new" | "workspace.terminal.new"
| "workspace.new" | "workspace.new"
@@ -9,6 +9,7 @@ import {
parseBindingChord, parseBindingChord,
resolveKeyboardShortcut, resolveKeyboardShortcut,
resolveShortcutKeysForAction, resolveShortcutKeysForAction,
SHORTCUT_HELP_ROW_ORDER,
UNASSIGNED_COMBO, UNASSIGNED_COMBO,
type ChordState, type ChordState,
type KeyboardShortcutContext, type KeyboardShortcutContext,
@@ -149,16 +150,16 @@ describe("keyboard-shortcuts", () => {
action: "workspace.new", action: "workspace.new",
}, },
{ {
name: "matches Cmd+P to switch project on mac", name: "matches Cmd+P to search workspace files on mac",
event: { key: "p", code: "KeyP", metaKey: true }, event: { key: "p", code: "KeyP", metaKey: true },
context: { isMac: true, commandCenterOpen: false }, context: { isMac: true, commandCenterOpen: false },
action: "workspace.project.pick", action: "command-center.files",
}, },
{ {
name: "matches Ctrl+P to switch project on non-mac", name: "matches Ctrl+P to search workspace files on non-mac",
event: { key: "p", code: "KeyP", ctrlKey: true }, event: { key: "p", code: "KeyP", ctrlKey: true },
context: { isMac: false, commandCenterOpen: false, focusScope: "other" }, context: { isMac: false, commandCenterOpen: false, focusScope: "other" },
action: "workspace.project.pick", action: "command-center.files",
}, },
{ {
name: "matches question-mark shortcut to toggle the shortcuts dialog", name: "matches question-mark shortcut to toggle the shortcuts dialog",
@@ -781,20 +782,63 @@ describe("keyboard-shortcut help sections", () => {
it("returns stable i18n keys for section titles and help rows", () => { it("returns stable i18n keys for section titles and help rows", () => {
const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true }); const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true });
const projects = sections.find((section) => section.id === "projects"); const workspaces = sections.find((section) => section.id === "workspaces");
const panels = sections.find((section) => section.id === "panels"); const layout = sections.find((section) => section.id === "layout");
const openProject = findRow(sections, "new-agent"); const openProject = findRow(sections, "new-agent");
const cycleAgentMode = findRow(sections, "cycle-agent-mode"); const cycleAgentMode = findRow(sections, "cycle-agent-mode");
const showShortcuts = findRow(sections, "show-shortcuts"); const showShortcuts = findRow(sections, "show-shortcuts");
expect(projects?.titleKey).toBe("settings.shortcuts.sections.projects"); expect(workspaces?.titleKey).toBe("settings.shortcuts.sections.workspaces");
expect(panels?.titleKey).toBe("settings.shortcuts.sections.panels"); expect(layout?.titleKey).toBe("settings.shortcuts.sections.layout");
expect(openProject?.labelKey).toBe("settings.shortcuts.help.openProject"); expect(openProject?.labelKey).toBe("settings.shortcuts.help.openProject");
expect(openProject?.label).toBe("Open project"); expect(openProject?.label).toBe("Open project");
expect(cycleAgentMode?.labelKey).toBe("settings.shortcuts.help.cycleAgentMode"); expect(cycleAgentMode?.labelKey).toBe("settings.shortcuts.help.cycleAgentMode");
expect(showShortcuts?.noteKey).toBe("settings.shortcuts.helpNotes.showKeyboardShortcuts"); expect(showShortcuts?.noteKey).toBe("settings.shortcuts.helpNotes.showKeyboardShortcuts");
}); });
it("gives every help row an explicit place in its section's order", () => {
const platforms = [
{ isMac: true, isDesktop: true },
{ isMac: false, isDesktop: true },
{ isMac: true, isDesktop: false },
{ isMac: false, isDesktop: false },
];
const unplaced: string[] = [];
for (const platform of platforms) {
for (const section of buildKeyboardShortcutHelpSections(platform)) {
for (const row of section.rows) {
if (SHORTCUT_HELP_ROW_ORDER[section.id].includes(row.id)) continue;
unplaced.push(`${section.id}:${row.id}`);
}
}
}
expect(unplaced).toEqual([]);
});
it("leads the general section with the command center and file search", () => {
const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true });
expect(sections[0]?.id).toBe("general");
expect(sections[0]?.rows.slice(0, 2).map((row) => row.id)).toEqual([
"toggle-command-center",
"search-files",
]);
});
it("reuses the project-picker binding ids for rebindable file search", () => {
expect(getBindingIdForAction("search-files", { isMac: true, isDesktop: true })).toBe(
"workspace-project-pick-cmd-p-mac",
);
expect(getBindingIdForAction("search-files", { isMac: false, isDesktop: true })).toBe(
"workspace-project-pick-ctrl-p-non-mac",
);
expect(
findRow(buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true }), "search-files")
?.chord,
).not.toBeNull();
});
it("does not expose Enter send behavior as rebindable shortcut rows", () => { it("does not expose Enter send behavior as rebindable shortcut rows", () => {
const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true }); const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true });
+125 -71
View File
@@ -49,7 +49,7 @@ export interface KeyboardShortcutHelpRow {
noteKey?: string; noteKey?: string;
} }
export type ShortcutSectionId = "navigation" | "tabs-panes" | "projects" | "panels" | "agent-input"; export type ShortcutSectionId = "general" | "workspaces" | "tabs-panes" | "layout" | "agent-input";
export interface KeyboardShortcutHelpSection { export interface KeyboardShortcutHelpSection {
id: ShortcutSectionId; id: ShortcutSectionId;
@@ -124,22 +124,81 @@ export interface ChordState {
// --- Constants --- // --- Constants ---
// Sections are listed in this order, most-reached-for first.
const SHORTCUT_HELP_SECTION_ORDER: readonly ShortcutSectionId[] = [
"general",
"workspaces",
"tabs-panes",
"layout",
"agent-input",
];
const SHORTCUT_HELP_SECTION_TITLES: Record<ShortcutSectionId, string> = { const SHORTCUT_HELP_SECTION_TITLES: Record<ShortcutSectionId, string> = {
navigation: "Navigation", general: "General",
workspaces: "Projects & Workspaces",
"tabs-panes": "Tabs & Panes", "tabs-panes": "Tabs & Panes",
projects: "Projects", layout: "Layout",
panels: "Panels",
"agent-input": "Agent Input", "agent-input": "Agent Input",
}; };
const SHORTCUT_HELP_SECTION_LABEL_KEYS: Record<ShortcutSectionId, string> = { const SHORTCUT_HELP_SECTION_LABEL_KEYS: Record<ShortcutSectionId, string> = {
navigation: "settings.shortcuts.sections.navigation", general: "settings.shortcuts.sections.general",
workspaces: "settings.shortcuts.sections.workspaces",
"tabs-panes": "settings.shortcuts.sections.tabsPanes", "tabs-panes": "settings.shortcuts.sections.tabsPanes",
projects: "settings.shortcuts.sections.projects", layout: "settings.shortcuts.sections.layout",
panels: "settings.shortcuts.sections.panels",
"agent-input": "settings.shortcuts.sections.agentInput", "agent-input": "settings.shortcuts.sections.agentInput",
}; };
// Rows render in this order rather than in binding-definition order, so the shortcut someone opens
// this sheet to find sits at the top of its section. Every help id must appear here; the unit test
// fails when a new shortcut is added without a place, instead of silently sinking to the bottom.
export const SHORTCUT_HELP_ROW_ORDER: Record<ShortcutSectionId, readonly string[]> = {
general: [
"toggle-command-center",
"search-files",
"show-shortcuts",
"toggle-settings",
"cycle-theme",
],
workspaces: [
"new-agent",
"new-workspace",
"workspace-jump-index",
"workspace-prev",
"workspace-next",
"pin-workspace",
"archive-workspace",
],
"tabs-panes": [
"workspace-tab-new",
"workspace-terminal-new",
"workspace-tab-close-current",
"workspace-tab-jump-index",
"workspace-tab-prev",
"workspace-tab-next",
"workspace-pane-split-right",
"workspace-pane-split-down",
"workspace-pane-focus-left",
"workspace-pane-focus-right",
"workspace-pane-focus-up",
"workspace-pane-focus-down",
"workspace-pane-move-tab-left",
"workspace-pane-move-tab-right",
"workspace-pane-move-tab-up",
"workspace-pane-move-tab-down",
"workspace-pane-close",
],
layout: ["toggle-left-sidebar", "toggle-right-sidebar", "toggle-both-sidebars", "toggle-focus"],
"agent-input": [
"focus-message-input",
"cycle-agent-mode",
"voice-toggle",
"dictation-toggle",
"agent-interrupt",
"voice-mute-toggle",
],
};
const SHORTCUT_HELP_LABEL_KEYS: Record<string, string> = { const SHORTCUT_HELP_LABEL_KEYS: Record<string, string> = {
"new-agent": "settings.shortcuts.help.openProject", "new-agent": "settings.shortcuts.help.openProject",
"new-workspace": "settings.shortcuts.help.newWorkspace", "new-workspace": "settings.shortcuts.help.newWorkspace",
@@ -165,6 +224,7 @@ const SHORTCUT_HELP_LABEL_KEYS: Record<string, string> = {
"workspace-pane-move-tab-down": "settings.shortcuts.help.moveTabDown", "workspace-pane-move-tab-down": "settings.shortcuts.help.moveTabDown",
"workspace-pane-close": "settings.shortcuts.help.closePane", "workspace-pane-close": "settings.shortcuts.help.closePane",
"workspace-terminal-new": "settings.shortcuts.help.newTerminal", "workspace-terminal-new": "settings.shortcuts.help.newTerminal",
"search-files": "settings.shortcuts.help.searchFiles",
"toggle-command-center": "settings.shortcuts.help.toggleCommandCenter", "toggle-command-center": "settings.shortcuts.help.toggleCommandCenter",
"show-shortcuts": "settings.shortcuts.help.showKeyboardShortcuts", "show-shortcuts": "settings.shortcuts.help.showKeyboardShortcuts",
"toggle-left-sidebar": "settings.shortcuts.help.toggleLeftSidebar", "toggle-left-sidebar": "settings.shortcuts.help.toggleLeftSidebar",
@@ -200,7 +260,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true }, when: { mac: true },
help: { help: {
id: "new-agent", id: "new-agent",
section: "projects", section: "workspaces",
label: "Open project", label: "Open project",
}, },
}, },
@@ -211,7 +271,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, terminal: false }, when: { mac: false, terminal: false },
help: { help: {
id: "new-agent", id: "new-agent",
section: "projects", section: "workspaces",
label: "Open project", label: "Open project",
}, },
}, },
@@ -224,7 +284,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "new-workspace", id: "new-workspace",
section: "projects", section: "workspaces",
label: "New workspace", label: "New workspace",
}, },
}, },
@@ -235,32 +295,32 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "new-workspace", id: "new-workspace",
section: "projects", section: "workspaces",
label: "New workspace", label: "New workspace",
}, },
}, },
// --- Switch project (New Workspace screen) --- // --- Search files (switch project on the New Workspace screen) ---
{ {
id: "workspace-project-pick-cmd-p-mac", id: "workspace-project-pick-cmd-p-mac",
action: "workspace.project.pick", action: "command-center.files",
combo: "Cmd+P", combo: "Cmd+P",
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "switch-project", id: "search-files",
section: "projects", section: "general",
label: "Switch project", label: "Search files",
}, },
}, },
{ {
id: "workspace-project-pick-ctrl-p-non-mac", id: "workspace-project-pick-ctrl-p-non-mac",
action: "workspace.project.pick", action: "command-center.files",
combo: "Ctrl+P", combo: "Ctrl+P",
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "switch-project", id: "search-files",
section: "projects", section: "general",
label: "Switch project", label: "Search files",
}, },
}, },
@@ -274,7 +334,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "archive-workspace", id: "archive-workspace",
section: "projects", section: "workspaces",
label: "Archive workspace", label: "Archive workspace",
}, },
}, },
@@ -287,7 +347,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "archive-workspace", id: "archive-workspace",
section: "projects", section: "workspaces",
label: "Archive workspace", label: "Archive workspace",
}, },
}, },
@@ -300,7 +360,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "pin-workspace", id: "pin-workspace",
section: "projects", section: "workspaces",
label: "Pin chat", label: "Pin chat",
}, },
}, },
@@ -311,7 +371,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "pin-workspace", id: "pin-workspace",
section: "projects", section: "workspaces",
label: "Pin chat", label: "Pin chat",
}, },
}, },
@@ -382,7 +442,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "index" }, payload: { type: "index" },
help: { help: {
id: "workspace-jump-index", id: "workspace-jump-index",
section: "navigation", section: "workspaces",
label: "Jump to workspace", label: "Jump to workspace",
defaultDisplayKeys: ["mod", "1-9"], defaultDisplayKeys: ["mod", "1-9"],
}, },
@@ -395,7 +455,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "index" }, payload: { type: "index" },
help: { help: {
id: "workspace-jump-index", id: "workspace-jump-index",
section: "navigation", section: "workspaces",
label: "Jump to workspace", label: "Jump to workspace",
defaultDisplayKeys: ["mod", "1-9"], defaultDisplayKeys: ["mod", "1-9"],
}, },
@@ -408,7 +468,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "index" }, payload: { type: "index" },
help: { help: {
id: "workspace-jump-index", id: "workspace-jump-index",
section: "navigation", section: "workspaces",
label: "Jump to workspace", label: "Jump to workspace",
defaultDisplayKeys: ["alt", "1-9"], defaultDisplayKeys: ["alt", "1-9"],
}, },
@@ -423,7 +483,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "index" }, payload: { type: "index" },
help: { help: {
id: "workspace-tab-jump-index", id: "workspace-tab-jump-index",
section: "navigation", section: "tabs-panes",
label: "Jump to tab", label: "Jump to tab",
defaultDisplayKeys: ["mod", "alt", "1-9"], defaultDisplayKeys: ["mod", "alt", "1-9"],
}, },
@@ -436,7 +496,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "index" }, payload: { type: "index" },
help: { help: {
id: "workspace-tab-jump-index", id: "workspace-tab-jump-index",
section: "navigation", section: "tabs-panes",
label: "Jump to tab", label: "Jump to tab",
defaultDisplayKeys: ["alt", "1-9"], defaultDisplayKeys: ["alt", "1-9"],
}, },
@@ -449,7 +509,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "index" }, payload: { type: "index" },
help: { help: {
id: "workspace-tab-jump-index", id: "workspace-tab-jump-index",
section: "navigation", section: "tabs-panes",
label: "Jump to tab", label: "Jump to tab",
defaultDisplayKeys: ["alt", "shift", "1-9"], defaultDisplayKeys: ["alt", "shift", "1-9"],
}, },
@@ -464,7 +524,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "delta", delta: -1 }, payload: { type: "delta", delta: -1 },
help: { help: {
id: "workspace-prev", id: "workspace-prev",
section: "navigation", section: "workspaces",
label: "Previous workspace", label: "Previous workspace",
}, },
}, },
@@ -476,7 +536,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "delta", delta: -1 }, payload: { type: "delta", delta: -1 },
help: { help: {
id: "workspace-prev", id: "workspace-prev",
section: "navigation", section: "workspaces",
label: "Previous workspace", label: "Previous workspace",
}, },
}, },
@@ -488,7 +548,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "delta", delta: 1 }, payload: { type: "delta", delta: 1 },
help: { help: {
id: "workspace-next", id: "workspace-next",
section: "navigation", section: "workspaces",
label: "Next workspace", label: "Next workspace",
}, },
}, },
@@ -500,7 +560,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "delta", delta: 1 }, payload: { type: "delta", delta: 1 },
help: { help: {
id: "workspace-next", id: "workspace-next",
section: "navigation", section: "workspaces",
label: "Next workspace", label: "Next workspace",
}, },
}, },
@@ -512,7 +572,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "delta", delta: -1 }, payload: { type: "delta", delta: -1 },
help: { help: {
id: "workspace-prev", id: "workspace-prev",
section: "navigation", section: "workspaces",
label: "Previous workspace", label: "Previous workspace",
}, },
}, },
@@ -524,7 +584,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "delta", delta: 1 }, payload: { type: "delta", delta: 1 },
help: { help: {
id: "workspace-next", id: "workspace-next",
section: "navigation", section: "workspaces",
label: "Next workspace", label: "Next workspace",
}, },
}, },
@@ -538,7 +598,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "delta", delta: -1 }, payload: { type: "delta", delta: -1 },
help: { help: {
id: "workspace-tab-prev", id: "workspace-tab-prev",
section: "navigation", section: "tabs-panes",
label: "Previous tab", label: "Previous tab",
}, },
}, },
@@ -550,7 +610,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
payload: { type: "delta", delta: 1 }, payload: { type: "delta", delta: 1 },
help: { help: {
id: "workspace-tab-next", id: "workspace-tab-next",
section: "navigation", section: "tabs-panes",
label: "Next tab", label: "Next tab",
}, },
}, },
@@ -686,7 +746,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "workspace-terminal-new", id: "workspace-terminal-new",
section: "panels", section: "tabs-panes",
label: "New terminal", label: "New terminal",
}, },
}, },
@@ -697,7 +757,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "workspace-terminal-new", id: "workspace-terminal-new",
section: "panels", section: "tabs-panes",
label: "New terminal", label: "New terminal",
}, },
}, },
@@ -710,7 +770,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true }, when: { mac: true },
help: { help: {
id: "toggle-command-center", id: "toggle-command-center",
section: "panels", section: "general",
label: "Toggle command center", label: "Toggle command center",
}, },
}, },
@@ -721,7 +781,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, terminal: false }, when: { mac: false, terminal: false },
help: { help: {
id: "toggle-command-center", id: "toggle-command-center",
section: "panels", section: "general",
label: "Toggle command center", label: "Toggle command center",
}, },
}, },
@@ -735,7 +795,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { focusScope: "other" }, when: { focusScope: "other" },
help: { help: {
id: "show-shortcuts", id: "show-shortcuts",
section: "panels", section: "general",
label: "Show keyboard shortcuts", label: "Show keyboard shortcuts",
defaultDisplayKeys: ["?"], defaultDisplayKeys: ["?"],
note: "Available when focus is not in a text field or terminal.", note: "Available when focus is not in a text field or terminal.",
@@ -750,7 +810,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true }, when: { mac: true },
help: { help: {
id: "toggle-left-sidebar", id: "toggle-left-sidebar",
section: "panels", section: "layout",
label: "Toggle left sidebar", label: "Toggle left sidebar",
}, },
}, },
@@ -761,7 +821,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "toggle-left-sidebar", id: "toggle-left-sidebar",
section: "panels", section: "layout",
label: "Toggle left sidebar", label: "Toggle left sidebar",
}, },
}, },
@@ -772,7 +832,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "toggle-right-sidebar", id: "toggle-right-sidebar",
section: "panels", section: "layout",
label: "Toggle right sidebar", label: "Toggle right sidebar",
}, },
}, },
@@ -783,7 +843,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "toggle-right-sidebar", id: "toggle-right-sidebar",
section: "panels", section: "layout",
label: "Toggle right sidebar", label: "Toggle right sidebar",
}, },
}, },
@@ -802,7 +862,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "toggle-both-sidebars", id: "toggle-both-sidebars",
section: "panels", section: "layout",
label: "Toggle both sidebars", label: "Toggle both sidebars",
}, },
}, },
@@ -813,7 +873,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "toggle-both-sidebars", id: "toggle-both-sidebars",
section: "panels", section: "layout",
label: "Toggle both sidebars", label: "Toggle both sidebars",
}, },
}, },
@@ -826,7 +886,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "toggle-settings", id: "toggle-settings",
section: "panels", section: "general",
label: "Toggle settings", label: "Toggle settings",
}, },
}, },
@@ -837,7 +897,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "toggle-settings", id: "toggle-settings",
section: "panels", section: "general",
label: "Toggle settings", label: "Toggle settings",
}, },
}, },
@@ -850,7 +910,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "toggle-focus", id: "toggle-focus",
section: "panels", section: "layout",
label: "Toggle focus mode", label: "Toggle focus mode",
}, },
}, },
@@ -861,7 +921,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "toggle-focus", id: "toggle-focus",
section: "panels", section: "layout",
label: "Toggle focus mode", label: "Toggle focus mode",
}, },
}, },
@@ -874,7 +934,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: true, commandCenter: false }, when: { mac: true, commandCenter: false },
help: { help: {
id: "cycle-theme", id: "cycle-theme",
section: "panels", section: "general",
label: "Cycle theme", label: "Cycle theme",
}, },
}, },
@@ -885,7 +945,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
when: { mac: false, commandCenter: false, terminal: false }, when: { mac: false, commandCenter: false, terminal: false },
help: { help: {
id: "cycle-theme", id: "cycle-theme",
section: "panels", section: "general",
label: "Cycle theme", label: "Cycle theme",
}, },
}, },
@@ -1504,13 +1564,9 @@ export function buildKeyboardShortcutHelpSections(
bindings: readonly ParsedShortcutBinding[] = DEFAULT_BINDINGS, bindings: readonly ParsedShortcutBinding[] = DEFAULT_BINDINGS,
): KeyboardShortcutHelpSection[] { ): KeyboardShortcutHelpSection[] {
const seenRows = new Set<string>(); const seenRows = new Set<string>();
const rowsBySection = new Map<ShortcutSectionId, KeyboardShortcutHelpRow[]>([ const rowsBySection = new Map<ShortcutSectionId, KeyboardShortcutHelpRow[]>(
["navigation", []], SHORTCUT_HELP_SECTION_ORDER.map((sectionId) => [sectionId, []]),
["tabs-panes", []], );
["projects", []],
["panels", []],
["agent-input", []],
]);
for (const binding of bindings) { for (const binding of bindings) {
const help = binding.help; const help = binding.help;
@@ -1540,19 +1596,17 @@ export function buildKeyboardShortcutHelpSections(
}); });
} }
const sectionOrder: ShortcutSectionId[] = [ return SHORTCUT_HELP_SECTION_ORDER.flatMap((sectionId) => {
"navigation",
"tabs-panes",
"projects",
"panels",
"agent-input",
];
return sectionOrder.flatMap((sectionId) => {
const rows = rowsBySection.get(sectionId) ?? []; const rows = rowsBySection.get(sectionId) ?? [];
if (rows.length === 0) { if (rows.length === 0) {
return []; return [];
} }
const order = SHORTCUT_HELP_ROW_ORDER[sectionId];
const rank = (row: KeyboardShortcutHelpRow) => {
const index = order.indexOf(row.id);
return index === -1 ? order.length : index;
};
rows.sort((left, right) => rank(left) - rank(right));
return [ return [
{ {
id: sectionId, id: sectionId,
@@ -351,6 +351,24 @@ describe("routeKeyboardShortcut — callbacks and pickers", () => {
}); });
describe("routeKeyboardShortcut — toggle dialogs", () => { describe("routeKeyboardShortcut — toggle dialogs", () => {
it("opens the command center scoped to files from a workspace", () => {
expect(
routeKeyboardShortcut({ action: "command-center.files", payload: null }, makeCtx()),
).toEqual<ShortcutAction>({ kind: "command-center-toggle", nextOpen: true, scope: "files" });
});
it("leaves the file-search shortcut to the project-picker host outside a workspace", () => {
expect(
routeKeyboardShortcut(
{ action: "command-center.files", payload: null },
makeCtx({ pathname: "/settings" }),
),
).toEqual<ShortcutAction>({
kind: "dispatch",
action: { id: "workspace.project.pick", scope: "workspace" },
});
});
it("opens the command center when closed", () => { it("opens the command center when closed", () => {
expect( expect(
routeKeyboardShortcut( routeKeyboardShortcut(
+6 -1
View File
@@ -32,7 +32,7 @@ export type ShortcutAction =
| { kind: "router-push"; route: string } | { kind: "router-push"; route: string }
| { kind: "open-project-picker" } | { kind: "open-project-picker" }
| { kind: "callback"; name: ShortcutCallbackName } | { kind: "callback"; name: ShortcutCallbackName }
| { kind: "command-center-toggle"; nextOpen: boolean } | { kind: "command-center-toggle"; nextOpen: boolean; scope?: "files" }
| { kind: "shortcuts-dialog-toggle"; nextOpen: boolean }; | { kind: "shortcuts-dialog-toggle"; nextOpen: boolean };
const NONE: ShortcutAction = { kind: "none" }; const NONE: ShortcutAction = { kind: "none" };
@@ -198,6 +198,11 @@ export function routeKeyboardShortcut(
return routeSettingsToggle(ctx); return routeSettingsToggle(ctx);
case "command-center.toggle": case "command-center.toggle":
return { kind: "command-center-toggle", nextOpen: !ctx.commandCenterOpen }; return { kind: "command-center-toggle", nextOpen: !ctx.commandCenterOpen };
case "command-center.files":
if (parseHostWorkspaceRouteFromPathname(ctx.pathname)) {
return { kind: "command-center-toggle", nextOpen: true, scope: "files" };
}
return dispatch({ id: "workspace.project.pick", scope: "workspace" });
case "shortcuts.dialog.toggle": case "shortcuts.dialog.toggle":
return { kind: "shortcuts-dialog-toggle", nextOpen: !ctx.shortcutsDialogOpen }; return { kind: "shortcuts-dialog-toggle", nextOpen: !ctx.shortcutsDialogOpen };
default: default:
@@ -99,12 +99,12 @@ describe("filterShortcutHelpSections", () => {
}); });
it("keeps every row of a section whose own title matches", () => { it("keeps every row of a section whose own title matches", () => {
const projects = filter("projects").find((section) => section.id === "projects"); const matched = filter("layout").find((section) => section.id === "layout");
const allProjects = buildKeyboardShortcutHelpSections(MAC_DESKTOP).find( const all = buildKeyboardShortcutHelpSections(MAC_DESKTOP).find(
(section) => section.id === "projects", (section) => section.id === "layout",
); );
expect(projects?.rows).toHaveLength(allProjects?.rows.length ?? 0); expect(matched?.rows).toHaveLength(all?.rows.length ?? 0);
}); });
it("drops a section whose rows all miss", () => { it("drops a section whose rows all miss", () => {
@@ -4,6 +4,7 @@ import { useKeyboardShortcutsStore } from "./keyboard-shortcuts-store";
beforeEach(() => { beforeEach(() => {
useKeyboardShortcutsStore.setState({ useKeyboardShortcutsStore.setState({
commandCenterOpen: false, commandCenterOpen: false,
commandCenterScope: null,
shortcutsDialogOpen: false, shortcutsDialogOpen: false,
capturingShortcut: false, capturingShortcut: false,
altDown: false, altDown: false,
@@ -19,6 +20,20 @@ describe("keyboard-shortcuts-store", () => {
expect(useKeyboardShortcutsStore.getState().commandCenterOpen).toBe(true); expect(useKeyboardShortcutsStore.getState().commandCenterOpen).toBe(true);
}); });
it("opens the command center with a scope and clears it when closed", () => {
useKeyboardShortcutsStore.getState().setCommandCenterOpen(true, "files");
expect(useKeyboardShortcutsStore.getState()).toMatchObject({
commandCenterOpen: true,
commandCenterScope: "files",
});
useKeyboardShortcutsStore.getState().setCommandCenterOpen(false);
expect(useKeyboardShortcutsStore.getState()).toMatchObject({
commandCenterOpen: false,
commandCenterScope: null,
});
});
it("toggles shortcut capture state", () => { it("toggles shortcut capture state", () => {
expect(useKeyboardShortcutsStore.getState().capturingShortcut).toBe(false); expect(useKeyboardShortcutsStore.getState().capturingShortcut).toBe(false);
useKeyboardShortcutsStore.getState().setCapturingShortcut(true); useKeyboardShortcutsStore.getState().setCapturingShortcut(true);
@@ -3,8 +3,11 @@ import type { SidebarShortcutWorkspaceTarget } from "@/utils/sidebar-shortcuts";
const SHORTCUT_BADGE_DELAY_MS = 150; const SHORTCUT_BADGE_DELAY_MS = 150;
export type CommandCenterScope = "files" | null;
interface KeyboardShortcutsState { interface KeyboardShortcutsState {
commandCenterOpen: boolean; commandCenterOpen: boolean;
commandCenterScope: CommandCenterScope;
shortcutsDialogOpen: boolean; shortcutsDialogOpen: boolean;
capturingShortcut: boolean; capturingShortcut: boolean;
altDown: boolean; altDown: boolean;
@@ -13,7 +16,8 @@ interface KeyboardShortcutsState {
/** Sidebar-visible workspace targets (up to 9), in top-to-bottom visual order. */ /** Sidebar-visible workspace targets (up to 9), in top-to-bottom visual order. */
sidebarShortcutWorkspaceTargets: SidebarShortcutWorkspaceTarget[]; sidebarShortcutWorkspaceTargets: SidebarShortcutWorkspaceTarget[];
setCommandCenterOpen: (open: boolean) => void; setCommandCenterOpen: (open: boolean, scope?: CommandCenterScope) => void;
setCommandCenterScope: (scope: CommandCenterScope) => void;
setShortcutsDialogOpen: (open: boolean) => void; setShortcutsDialogOpen: (open: boolean) => void;
setCapturingShortcut: (capturing: boolean) => void; setCapturingShortcut: (capturing: boolean) => void;
setAltDown: (down: boolean) => void; setAltDown: (down: boolean) => void;
@@ -47,6 +51,7 @@ function updateBadgeTimer(
export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>((set, get) => ({ export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>((set, get) => ({
commandCenterOpen: false, commandCenterOpen: false,
commandCenterScope: null,
shortcutsDialogOpen: false, shortcutsDialogOpen: false,
capturingShortcut: false, capturingShortcut: false,
altDown: false, altDown: false,
@@ -54,7 +59,9 @@ export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>((set, ge
showShortcutBadges: false, showShortcutBadges: false,
sidebarShortcutWorkspaceTargets: [], sidebarShortcutWorkspaceTargets: [],
setCommandCenterOpen: (open) => set({ commandCenterOpen: open }), setCommandCenterOpen: (open, scope = null) =>
set({ commandCenterOpen: open, commandCenterScope: open ? scope : null }),
setCommandCenterScope: (scope) => set({ commandCenterScope: scope }),
setShortcutsDialogOpen: (open) => set({ shortcutsDialogOpen: open }), setShortcutsDialogOpen: (open) => set({ shortcutsDialogOpen: open }),
setCapturingShortcut: (capturing) => set({ capturingShortcut: capturing }), setCapturingShortcut: (capturing) => set({ capturingShortcut: capturing }),
setAltDown: (down) => { setAltDown: (down) => {