feat(web): add reverse direction to recent task switcher (#1241)

* feat(web): add reverse direction to recent task switcher

Cmd/Ctrl+Shift+Space now cycles the recent task switcher backward
(most-recent toward oldest), complementing the forward Cmd/Ctrl+Space.
Both directions are user-configurable via Settings > Keyboard Shortcuts.

- new TASK_SWITCHER_REVERSE shortcut; Shift keeps it mutually exclusive
  with the forward binding so both can be held together
- thread a cycle direction through openSwitcher/cycleSwitcher; reverse
  opens on the oldest non-current task
- commit on release of either binding's hold modifier
- switcher footer shows both Next / Previous bindings

* fix(web): commit task switcher only on the active binding's modifier

Address PR review on the reverse task switcher:

- track the binding that drove the switcher and commit only on ITS hold
  modifier release, so divergent custom forward/reverse bindings can't
  commit early (greptile/coderabbit)
- move the CycleDirection type below the import block
- correct the reverse-shortcut direction comment (oldest -> most-recent)
- assert exact task titles in the reverse e2e precondition

* test(web): assert task switcher stays open on Shift-release

Make explicit that releasing the secondary modifier alone does not commit
the reverse switcher — only the hold modifier does.
This commit is contained in:
José Almeida
2026-06-02 13:44:17 +01:00
committed by GitHub
parent f149bd1292
commit b0b968cb22
10 changed files with 335 additions and 55 deletions
@@ -27,6 +27,7 @@ import {
import {
buildRecentTaskDisplayItems,
buildRecentTaskEntry,
getInitialReverseSelectionIndex,
getInitialSelectionIndex,
getNextSelectionIndex,
getPreviousSelectionIndex,
@@ -39,6 +40,9 @@ import {
isCycleShortcutEvent,
} from "./recent-task-switcher-keys";
/** Direction the switcher cycles through recent tasks. */
type CycleDirection = "forward" | "backward";
export type RecentTaskSwitcherController = {
open: boolean;
setOpen: (open: boolean) => void;
@@ -46,6 +50,7 @@ export type RecentTaskSwitcherController = {
selectedIndex: number;
setSelectedIndex: (index: number) => void;
shortcutLabel: string;
reverseShortcutLabel: string;
selectItem: (item: RecentTaskDisplayItem | undefined) => void;
handleKeyDown: (event: ReactKeyboardEvent) => void;
};
@@ -58,14 +63,19 @@ type SwitcherRefs = {
itemsRef: MutableRefObject<RecentTaskDisplayItem[]>;
activeTaskIdRef: MutableRefObject<string | null>;
shortcutRef: MutableRefObject<KeyboardShortcut>;
reverseShortcutRef: MutableRefObject<KeyboardShortcut>;
// The binding whose hold-modifier release should commit — set to the shortcut
// of the direction that opened/last drove the switcher, so a divergent custom
// binding for the other direction can't commit early.
activeCommitShortcutRef: MutableRefObject<KeyboardShortcut | null>;
};
type SwitcherActions = {
setOpen: (open: boolean) => void;
setSelectedIndex: (index: number) => void;
selectItem: (item: RecentTaskDisplayItem | undefined) => void;
openSwitcher: (commitOnRelease: boolean) => void;
cycleSwitcher: (commitOnRelease: boolean) => void;
openSwitcher: (commitOnRelease: boolean, direction?: CycleDirection) => void;
cycleSwitcher: (commitOnRelease: boolean, direction?: CycleDirection) => void;
cancelSwitcher: () => void;
selectCurrentItem: () => void;
};
@@ -182,13 +192,21 @@ function useLatestRef<T>(value: T) {
return ref;
}
function useSwitcherRefs(
open: boolean,
selectedIndex: number,
items: RecentTaskDisplayItem[],
activeTaskId: string | null,
shortcut: KeyboardShortcut,
): SwitcherRefs {
function useSwitcherRefs({
open,
selectedIndex,
items,
activeTaskId,
shortcut,
reverseShortcut,
}: {
open: boolean;
selectedIndex: number;
items: RecentTaskDisplayItem[];
activeTaskId: string | null;
shortcut: KeyboardShortcut;
reverseShortcut: KeyboardShortcut;
}): SwitcherRefs {
const openRef = useRef(open);
const selectedIndexRef = useRef(selectedIndex);
const commitOnReleaseRef = useRef(false);
@@ -196,6 +214,8 @@ function useSwitcherRefs(
const itemsRef = useLatestRef(items);
const activeTaskIdRef = useLatestRef(activeTaskId);
const shortcutRef = useLatestRef(shortcut);
const reverseShortcutRef = useLatestRef(reverseShortcut);
const activeCommitShortcutRef = useRef<KeyboardShortcut | null>(null);
useEffect(() => {
openRef.current = open;
@@ -210,6 +230,8 @@ function useSwitcherRefs(
itemsRef,
activeTaskIdRef,
shortcutRef,
reverseShortcutRef,
activeCommitShortcutRef,
};
}
@@ -228,6 +250,95 @@ function useSelectedIndexSetter(refs: SwitcherRefs, setRawSelectedIndex: (index:
);
}
type SwitcherCycleActions = Pick<SwitcherActions, "openSwitcher" | "cycleSwitcher">;
function useSwitcherCycleActions({
refs,
setCommandPanelOpen,
setOpenState,
setSelectedIndex,
}: {
refs: SwitcherRefs;
setCommandPanelOpen: (open: boolean) => void;
setOpenState: (open: boolean) => void;
setSelectedIndex: (index: number) => void;
}): SwitcherCycleActions {
const {
activeCommitShortcutRef,
activeTaskIdRef,
cancelledRef,
commitOnReleaseRef,
itemsRef,
openRef,
reverseShortcutRef,
selectedIndexRef,
shortcutRef,
} = refs;
const commitShortcutForDirection = useCallback(
(direction: CycleDirection) =>
direction === "backward" ? reverseShortcutRef.current : shortcutRef.current,
[reverseShortcutRef, shortcutRef],
);
const openSwitcher = useCallback(
(commitOnRelease: boolean, direction: CycleDirection = "forward") => {
setCommandPanelOpen(false);
cancelledRef.current = false;
commitOnReleaseRef.current = commitOnRelease;
activeCommitShortcutRef.current = commitShortcutForDirection(direction);
openRef.current = true;
const initialIndex =
direction === "backward"
? getInitialReverseSelectionIndex(itemsRef.current, activeTaskIdRef.current)
: getInitialSelectionIndex(itemsRef.current, activeTaskIdRef.current);
setSelectedIndex(initialIndex);
setOpenState(true);
},
[
activeCommitShortcutRef,
activeTaskIdRef,
cancelledRef,
commitOnReleaseRef,
commitShortcutForDirection,
itemsRef,
openRef,
setCommandPanelOpen,
setOpenState,
setSelectedIndex,
],
);
const cycleSwitcher = useCallback(
(commitOnRelease: boolean, direction: CycleDirection = "forward") => {
if (!openRef.current) {
openSwitcher(commitOnRelease, direction);
return;
}
if (commitOnRelease) commitOnReleaseRef.current = true;
activeCommitShortcutRef.current = commitShortcutForDirection(direction);
const count = itemsRef.current.length;
const nextIndex =
direction === "backward"
? getPreviousSelectionIndex(selectedIndexRef.current, count)
: getNextSelectionIndex(selectedIndexRef.current, count);
setSelectedIndex(nextIndex);
},
[
activeCommitShortcutRef,
commitOnReleaseRef,
commitShortcutForDirection,
itemsRef,
openRef,
openSwitcher,
selectedIndexRef,
setSelectedIndex,
],
);
return { openSwitcher, cycleSwitcher };
}
function useSwitcherActions({
refs,
routeToTask,
@@ -241,14 +352,27 @@ function useSwitcherActions({
setOpenState: (open: boolean) => void;
setSelectedIndex: (index: number) => void;
}): SwitcherActions {
const { activeTaskIdRef, cancelledRef, commitOnReleaseRef, itemsRef, openRef, selectedIndexRef } =
refs;
const {
activeCommitShortcutRef,
cancelledRef,
commitOnReleaseRef,
itemsRef,
openRef,
selectedIndexRef,
} = refs;
const { openSwitcher, cycleSwitcher } = useSwitcherCycleActions({
refs,
setCommandPanelOpen,
setOpenState,
setSelectedIndex,
});
const closeSwitcher = useCallback(() => {
openRef.current = false;
commitOnReleaseRef.current = false;
activeCommitShortcutRef.current = null;
setOpenState(false);
}, [commitOnReleaseRef, openRef, setOpenState]);
}, [activeCommitShortcutRef, commitOnReleaseRef, openRef, setOpenState]);
const cancelSwitcher = useCallback(() => {
cancelledRef.current = true;
@@ -276,39 +400,6 @@ function useSwitcherActions({
[closeSwitcher, routeToTask],
);
const openSwitcher = useCallback(
(commitOnRelease: boolean) => {
setCommandPanelOpen(false);
cancelledRef.current = false;
commitOnReleaseRef.current = commitOnRelease;
openRef.current = true;
setSelectedIndex(getInitialSelectionIndex(itemsRef.current, activeTaskIdRef.current));
setOpenState(true);
},
[
activeTaskIdRef,
cancelledRef,
commitOnReleaseRef,
itemsRef,
openRef,
setCommandPanelOpen,
setOpenState,
setSelectedIndex,
],
);
const cycleSwitcher = useCallback(
(commitOnRelease: boolean) => {
if (!openRef.current) {
openSwitcher(commitOnRelease);
return;
}
if (commitOnRelease) commitOnReleaseRef.current = true;
setSelectedIndex(getNextSelectionIndex(selectedIndexRef.current, itemsRef.current.length));
},
[commitOnReleaseRef, itemsRef, openRef, openSwitcher, selectedIndexRef, setSelectedIndex],
);
const selectCurrentItem = useCallback(() => {
selectItem(itemsRef.current[selectedIndexRef.current]);
}, [itemsRef, selectItem, selectedIndexRef]);
@@ -342,7 +433,14 @@ function useRecentTaskSwitcherCommand(shortcut: KeyboardShortcut, openSwitcher:
}
function useSwitcherGlobalKeyboard(refs: SwitcherRefs, actions: SwitcherActions) {
const { cancelledRef, commitOnReleaseRef, openRef, shortcutRef } = refs;
const {
activeCommitShortcutRef,
cancelledRef,
commitOnReleaseRef,
openRef,
reverseShortcutRef,
shortcutRef,
} = refs;
const { cancelSwitcher, cycleSwitcher, selectCurrentItem } = actions;
useEffect(() => {
@@ -354,18 +452,33 @@ function useSwitcherGlobalKeyboard(refs: SwitcherRefs, actions: SwitcherActions)
return;
}
// Check the reverse shortcut first: it carries an extra modifier (Shift),
// so it is the more specific match and the forward shortcut never matches
// a Shift-held event anyway.
const reverseShortcut = reverseShortcutRef.current;
if (isCycleShortcutEvent(event, reverseShortcut)) {
event.preventDefault();
event.stopPropagation();
cycleSwitcher(hasHoldModifier(reverseShortcut), "backward");
return;
}
const currentShortcut = shortcutRef.current;
if (!isCycleShortcutEvent(event, currentShortcut)) return;
event.preventDefault();
event.stopPropagation();
cycleSwitcher(hasHoldModifier(currentShortcut));
cycleSwitcher(hasHoldModifier(currentShortcut), "forward");
};
const handleKeyUp = (event: KeyboardEvent) => {
const currentShortcut = shortcutRef.current;
if (!openRef.current || !commitOnReleaseRef.current) return;
if (!isCommitReleaseEvent(event, currentShortcut)) return;
// Commit only when the hold modifier of the binding that drove the
// switcher is released. Both directions default to Ctrl/Cmd, but custom
// bindings may differ — releasing the other direction's modifier must not
// commit early.
const activeShortcut = activeCommitShortcutRef.current;
if (!activeShortcut || !isCommitReleaseEvent(event, activeShortcut)) return;
event.preventDefault();
event.stopPropagation();
@@ -396,11 +509,13 @@ function useSwitcherGlobalKeyboard(refs: SwitcherRefs, actions: SwitcherActions)
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [
activeCommitShortcutRef,
cancelSwitcher,
cancelledRef,
commitOnReleaseRef,
cycleSwitcher,
openRef,
reverseShortcutRef,
selectCurrentItem,
shortcutRef,
]);
@@ -451,12 +566,20 @@ export function useRecentTaskSwitcherController(): RecentTaskSwitcherController
const { setOpen: setCommandPanelOpen } = useCommandPanelOpen();
const keyboardShortcuts = useAppStore((state) => state.userSettings.keyboardShortcuts);
const shortcut = getShortcut("TASK_SWITCHER", keyboardShortcuts);
const reverseShortcut = getShortcut("TASK_SWITCHER_REVERSE", keyboardShortcuts);
const context = useRecentTaskBuildContext();
useRecordActiveTask(context);
const items = useMemo(() => buildRecentTaskDisplayItems(entries, context), [entries, context]);
const selectedIndex = getResolvedSelectionIndex(rawSelectedIndex, items, context.activeTaskId);
const refs = useSwitcherRefs(open, selectedIndex, items, context.activeTaskId, shortcut);
const refs = useSwitcherRefs({
open,
selectedIndex,
items,
activeTaskId: context.activeTaskId,
shortcut,
reverseShortcut,
});
const setSelectedIndex = useSelectedIndexSetter(refs, setRawSelectedIndex);
const routeToTask = useCallback((taskId: string) => router.push(linkToTask(taskId)), [router]);
const actions = useSwitcherActions({
@@ -479,6 +602,7 @@ export function useRecentTaskSwitcherController(): RecentTaskSwitcherController
selectedIndex,
setSelectedIndex,
shortcutLabel: formatShortcut(shortcut),
reverseShortcutLabel: formatShortcut(reverseShortcut),
selectItem: actions.selectItem,
handleKeyDown,
};
@@ -24,6 +24,11 @@ const taskSwitcherShortcut: KeyboardShortcut = {
modifiers: { ctrlOrCmd: true },
};
const taskSwitcherReverseShortcut: KeyboardShortcut = {
key: KEYS.SPACE,
modifiers: { ctrlOrCmd: true, shift: true },
};
function platform(platform: Platform): Platform {
return platform;
}
@@ -70,6 +75,30 @@ describe("recent task switcher key helpers", () => {
expect(isCycleShortcutEvent(event({ key: "y", ctrlKey: true }), shortcut)).toBe(false);
});
it("separates forward and reverse switcher shortcuts by the Shift modifier", () => {
const forwardEvent = event({ key: KEYS.SPACE, ctrlKey: true });
const reverseEvent = event({ key: KEYS.SPACE, ctrlKey: true, shiftKey: true });
// Forward fires only without Shift; reverse fires only with Shift.
expect(isCycleShortcutEvent(forwardEvent, taskSwitcherShortcut)).toBe(true);
expect(isCycleShortcutEvent(forwardEvent, taskSwitcherReverseShortcut)).toBe(false);
expect(isCycleShortcutEvent(reverseEvent, taskSwitcherShortcut)).toBe(false);
expect(isCycleShortcutEvent(reverseEvent, taskSwitcherReverseShortcut)).toBe(true);
});
it("commits the reverse shortcut on Ctrl/Cmd release, not Shift release", () => {
expect(
isCommitReleaseEvent(
event({ key: "Control" }),
taskSwitcherReverseShortcut,
platform("linux"),
),
).toBe(true);
expect(
isCommitReleaseEvent(event({ key: "Shift" }), taskSwitcherReverseShortcut, platform("linux")),
).toBe(false);
});
it("matches the hold modifier release, not secondary modifier release", () => {
const shortcut: KeyboardShortcut = {
key: KEYS.Y,
@@ -11,6 +11,7 @@ import {
import {
buildRecentTaskDisplayItems,
buildRecentTaskEntry,
getInitialReverseSelectionIndex,
getInitialSelectionIndex,
getNextSelectionIndex,
getPreviousSelectionIndex,
@@ -134,6 +135,26 @@ describe("recent task switcher model", () => {
expect(getInitialSelectionIndex([], CURRENT_TASK_ID)).toBe(-1);
});
it("starts reverse selection on the last non-current task", () => {
const items = [
{ taskId: CURRENT_TASK_ID },
{ taskId: PREVIOUS_TASK_ID },
{ taskId: "task-older" },
];
expect(getInitialReverseSelectionIndex(items, CURRENT_TASK_ID)).toBe(2);
// When the current task is the last item, skip back to the previous one.
expect(
getInitialReverseSelectionIndex(
[{ taskId: PREVIOUS_TASK_ID }, { taskId: CURRENT_TASK_ID }],
CURRENT_TASK_ID,
),
).toBe(0);
// All items are current -> fall back to the last index.
expect(getInitialReverseSelectionIndex([{ taskId: CURRENT_TASK_ID }], CURRENT_TASK_ID)).toBe(0);
expect(getInitialReverseSelectionIndex([], CURRENT_TASK_ID)).toBe(-1);
});
it("cycles through items and handles empty lists", () => {
expect(getNextSelectionIndex(0, 3)).toBe(1);
expect(getNextSelectionIndex(2, 3)).toBe(0);
@@ -362,6 +362,17 @@ export function getInitialSelectionIndex(
return firstNonCurrent === -1 ? 0 : firstNonCurrent;
}
export function getInitialReverseSelectionIndex(
items: Array<{ taskId: string }>,
currentTaskId: string | null,
): number {
if (items.length === 0) return -1;
for (let index = items.length - 1; index >= 0; index--) {
if (items[index].taskId !== currentTaskId) return index;
}
return items.length - 1;
}
export function getNextSelectionIndex(currentIndex: number, itemCount: number): number {
if (itemCount === 0) return -1;
if (currentIndex < 0) return 0;
@@ -128,10 +128,21 @@ function RecentTaskList({
);
}
function SwitcherFooter({ shortcutLabel }: { shortcutLabel: string }) {
function SwitcherFooter({
shortcutLabel,
reverseShortcutLabel,
}: {
shortcutLabel: string;
reverseShortcutLabel: string;
}) {
return (
<div className="border-t px-4 py-2 text-[11px] text-muted-foreground">
<Kbd>{shortcutLabel}</Kbd>
<div className="flex items-center gap-3 border-t px-4 py-2 text-[11px] text-muted-foreground">
<span className="flex items-center gap-1.5">
<Kbd>{shortcutLabel}</Kbd> Next
</span>
<span className="flex items-center gap-1.5">
<Kbd>{reverseShortcutLabel}</Kbd> Previous
</span>
</div>
);
}
@@ -160,7 +171,10 @@ function RecentTaskSwitcherDialog(props: RecentTaskSwitcherController) {
selectItem={props.selectItem}
/>
</div>
<SwitcherFooter shortcutLabel={props.shortcutLabel} />
<SwitcherFooter
shortcutLabel={props.shortcutLabel}
reverseShortcutLabel={props.reverseShortcutLabel}
/>
</DialogContent>
</Dialog>
);
@@ -19,6 +19,7 @@ test.describe("Keyboard Shortcuts Settings", () => {
await expect(testPage.getByTestId("shortcut-recorder-FOCUS_INPUT")).toBeVisible();
await expect(testPage.getByTestId("shortcut-recorder-TOGGLE_PLAN_MODE")).toBeVisible();
await expect(testPage.getByTestId("shortcut-recorder-TASK_SWITCHER")).toBeVisible();
await expect(testPage.getByTestId("shortcut-recorder-TASK_SWITCHER_REVERSE")).toBeVisible();
});
test("can record a new shortcut and persist it", async ({ testPage, apiClient, seedData }) => {
@@ -68,6 +68,67 @@ test.describe("Recent task switcher", () => {
await expect(testPage).toHaveURL(new RegExp(`/t/${first.id}`), { timeout: 10_000 });
});
test("reverse shortcut cycles backward through recent tasks", async ({
testPage,
apiClient,
seedData,
}) => {
const first = await apiClient.createTask(seedData.workspaceId, "Reverse Switcher One", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const second = await apiClient.createTask(seedData.workspaceId, "Reverse Switcher Two", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const third = await apiClient.createTask(seedData.workspaceId, "Reverse Switcher Three", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
// Visit oldest -> newest so the recent list is [third(current), second, first].
const titleByTask = new Map([
[first.id, "Reverse Switcher One"],
[second.id, "Reverse Switcher Two"],
[third.id, "Reverse Switcher Three"],
]);
for (const task of [first, second, third]) {
await testPage.goto(`/t/${task.id}`);
await expect(
testPage.getByText(titleByTask.get(task.id)!, { exact: true }).first(),
).toBeVisible({ timeout: 15_000 });
}
const modifier = taskSwitcherModifier();
await testPage.keyboard.down(modifier);
await testPage.keyboard.down("Shift");
await testPage.keyboard.press("Space");
const switcher = testPage.getByTestId("recent-task-switcher");
await expect(switcher).toBeVisible({ timeout: 5_000 });
// Reverse opens on the oldest non-current task (first).
const firstRow = switcher.getByTestId(`recent-task-switcher-item-${first.id}`);
const secondRow = switcher.getByTestId(`recent-task-switcher-item-${second.id}`);
await expect(firstRow).toHaveAttribute("data-selected", "true");
// Another reverse step moves selection backward to the newer "second" task.
await testPage.keyboard.press("Space");
await expect(secondRow).toHaveAttribute("data-selected", "true");
// Releasing Shift alone must not commit — only the hold modifier does.
await testPage.keyboard.up("Shift");
await expect(switcher).toBeVisible();
await expect(secondRow).toHaveAttribute("data-selected", "true");
await testPage.keyboard.up(modifier);
await expect(switcher).not.toBeVisible({ timeout: 5_000 });
await expect(testPage).toHaveURL(new RegExp(`/t/${second.id}`), { timeout: 10_000 });
});
test("escape cancels the held switcher without routing on release", async ({
testPage,
apiClient,
+6
View File
@@ -145,6 +145,12 @@ export const SHORTCUTS = {
key: KEYS.SPACE,
modifiers: { ctrlOrCmd: true },
},
// Cycle the recent-task switcher backward (oldest -> most-recent). Shift
// distinguishes it from TASK_SWITCHER so both can be held together.
TASK_SWITCHER_REVERSE: {
key: KEYS.SPACE,
modifiers: { ctrlOrCmd: true, shift: true },
},
BOTTOM_TERMINAL: {
key: KEYS.J,
modifiers: { ctrlOrCmd: true },
@@ -20,8 +20,9 @@ describe("CONFIGURABLE_SHORTCUTS", () => {
expect(ids).toContain("FOCUS_INPUT");
expect(ids).toContain("TOGGLE_PLAN_MODE");
expect(ids).toContain("TASK_SWITCHER");
expect(ids).toContain("TASK_SWITCHER_REVERSE");
expect(ids).toContain("VOICE_INPUT_TOGGLE");
expect(ids).toHaveLength(11);
expect(ids).toHaveLength(12);
});
it("each entry has a label and default matching SHORTCUTS", () => {
@@ -45,6 +46,13 @@ describe("CONFIGURABLE_SHORTCUTS", () => {
expect(CONFIGURABLE_SHORTCUTS.TASK_SWITCHER.label).toBe("Recent Task Switcher");
expect(CONFIGURABLE_SHORTCUTS.TASK_SWITCHER.default).toBe(SHORTCUTS.TASK_SWITCHER);
expect(CONFIGURABLE_SHORTCUTS.TASK_SWITCHER_REVERSE.label).toBe(
"Recent Task Switcher (Backward)",
);
expect(CONFIGURABLE_SHORTCUTS.TASK_SWITCHER_REVERSE.default).toBe(
SHORTCUTS.TASK_SWITCHER_REVERSE,
);
});
});
@@ -11,6 +11,7 @@ export type ConfigurableShortcutId =
| "FOCUS_INPUT"
| "TOGGLE_PLAN_MODE"
| "TASK_SWITCHER"
| "TASK_SWITCHER_REVERSE"
| "VOICE_INPUT_TOGGLE";
export type StoredShortcutOverrides = Record<
@@ -32,6 +33,10 @@ export const CONFIGURABLE_SHORTCUTS: Record<
FOCUS_INPUT: { label: "Focus Chat Input", default: SHORTCUTS.FOCUS_INPUT },
TOGGLE_PLAN_MODE: { label: "Toggle Plan Mode", default: SHORTCUTS.TOGGLE_PLAN_MODE },
TASK_SWITCHER: { label: "Recent Task Switcher", default: SHORTCUTS.TASK_SWITCHER },
TASK_SWITCHER_REVERSE: {
label: "Recent Task Switcher (Backward)",
default: SHORTCUTS.TASK_SWITCHER_REVERSE,
},
VOICE_INPUT_TOGGLE: { label: "Voice Input", default: SHORTCUTS.VOICE_INPUT_TOGGLE },
};