Files
paseo/packages/app/src/command-center/root-registration.tsx
T
07044136c8 feat(app): switch sidebar Group by from the command center (#3063)
Switching the sidebar between Project and Status grouping takes three
clicks today: open the display preferences menu, open Grouping, pick the
mode. It is the only sidebar preference with no fast path.

Typing "group" in the command center now offers a single entry that
switches to the mode you are not in, so it reads "Group by status" while
grouped by project and the reverse once you are in status mode. With
two modes a "cycle grouping" label would not say where you land, and an
entry that reads the same in both states looks like it does nothing.

The entry is query-only, so the default Cmd-K list is unchanged. It does
not clear the focus restore element, because it navigates nowhere and
focus has to return to whatever you were typing in.

The contribution is built by a pure function in root-contributions.ts so
its branches can be unit tested, mirroring workspace-contributions.ts.
The registration subscribes to groupMode with a narrow selector to avoid
re-registering every root action when host filters are reconciled.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:35:10 +00:00

284 lines
9.4 KiB
TypeScript

import { useMemo } from "react";
import { router, type Href } from "expo-router";
import { useTranslation } from "react-i18next";
import {
CalendarClock,
CircleDashed,
Folder,
FolderPlus,
History,
Home,
Keyboard,
Plus,
Settings,
} from "lucide-react-native";
import { withUnistyles } from "react-native-unistyles";
import { getIsElectronRuntime } from "@/constants/layout";
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
import { useOpenAddProject } from "@/hooks/use-open-add-project";
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
import { useKeyboardShortcutsAvailable } from "@/keyboard/availability";
import { resolveShortcutKeysForAction } from "@/keyboard/keyboard-shortcuts";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { useSidebarViewStore } from "@/stores/sidebar-view-store";
import { clearCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
import {
buildOpenProjectRoute,
buildSchedulesRoute,
buildSessionsRoute,
buildSettingsRoute,
} from "@/utils/host-routes";
import { getShortcutOs } from "@/utils/shortcut-platform";
import type { CommandCenterContribution, CommandCenterIconProps } from "./contributions";
import { useCommandCenterActions } from "./provider";
import { buildGroupingContribution } from "./root-contributions";
const ThemedPlus = withUnistyles(Plus, (theme) => ({ color: theme.colors.foregroundMuted }));
const ThemedFolderPlus = withUnistyles(FolderPlus, (theme) => ({
color: theme.colors.foregroundMuted,
}));
const ThemedHistory = withUnistyles(History, (theme) => ({
color: theme.colors.foregroundMuted,
}));
const ThemedCalendarClock = withUnistyles(CalendarClock, (theme) => ({
color: theme.colors.foregroundMuted,
}));
const ThemedKeyboard = withUnistyles(Keyboard, (theme) => ({
color: theme.colors.foregroundMuted,
}));
const ThemedSettings = withUnistyles(Settings, (theme) => ({
color: theme.colors.foregroundMuted,
}));
const ThemedHome = withUnistyles(Home, (theme) => ({ color: theme.colors.foregroundMuted }));
const ThemedFolder = withUnistyles(Folder, (theme) => ({ color: theme.colors.foregroundMuted }));
const ThemedCircleDashed = withUnistyles(CircleDashed, (theme) => ({
color: theme.colors.foregroundMuted,
}));
function PlusIcon({ size }: CommandCenterIconProps) {
return <ThemedPlus size={size} strokeWidth={2.4} />;
}
function AddProjectIcon({ size }: CommandCenterIconProps) {
return <ThemedFolderPlus size={size} strokeWidth={2.2} />;
}
function SettingsIcon({ size }: CommandCenterIconProps) {
return <ThemedSettings size={size} strokeWidth={2.2} />;
}
function HistoryIcon({ size }: CommandCenterIconProps) {
return <ThemedHistory size={size} strokeWidth={2.2} />;
}
function SchedulesIcon({ size }: CommandCenterIconProps) {
return <ThemedCalendarClock size={size} strokeWidth={2.2} />;
}
function KeyboardIcon({ size }: CommandCenterIconProps) {
return <ThemedKeyboard size={size} strokeWidth={2.2} />;
}
function HomeIcon({ size }: CommandCenterIconProps) {
return <ThemedHome size={size} strokeWidth={2.2} />;
}
function FolderIcon({ size }: CommandCenterIconProps) {
return <ThemedFolder size={size} strokeWidth={2.2} />;
}
function CircleDashedIcon({ size }: CommandCenterIconProps) {
return <ThemedCircleDashed size={size} strokeWidth={2.2} />;
}
export function CommandCenterRootActions() {
const { t } = useTranslation();
const { overrides } = useKeyboardShortcutOverrides();
const shortcutsAvailable = useKeyboardShortcutsAvailable();
const openAddProject = useOpenAddProject();
const settingsRoute = useMemo<Href>(() => buildSettingsRoute(), []);
const homeRoute = useMemo<Href>(() => buildOpenProjectRoute(), []);
const sessionsRoute = useMemo<Href>(() => buildSessionsRoute(), []);
const schedulesRoute = useMemo<Href>(() => buildSchedulesRoute(), []);
const setShortcutsDialogOpen = useKeyboardShortcutsStore((state) => state.setShortcutsDialogOpen);
// Narrow selector on purpose: a whole-store subscription would re-register every root action
// each time host filters are reconciled.
const groupMode = useSidebarViewStore((state) => state.groupMode);
const setGroupMode = useSidebarViewStore((state) => state.setGroupMode);
const shortcutPlatform = useMemo(
() => ({ isMac: getShortcutOs() === "mac", isDesktop: getIsElectronRuntime() }),
[],
);
const actions = useMemo<CommandCenterContribution[]>(() => {
const availableActions: CommandCenterContribution[] = [
{
id: "add-project",
group: "actions",
groupRank: 0,
rank: 0,
keywords: ["open", "project", "folder", "workspace", "repo"],
visibility: "query",
run: () => {
clearCommandCenterFocusRestoreElement();
openAddProject();
},
presentation: {
kind: "action",
title: t("shell.commandCenter.addProject"),
sectionTitle: t("shell.commandCenter.actions"),
icon: AddProjectIcon,
// The help id remains "new-agent" because user overrides are keyed by its binding id.
shortcutKeys:
resolveShortcutKeysForAction("new-agent", overrides, shortcutPlatform) ?? undefined,
},
},
{
id: "new-workspace",
group: "actions",
groupRank: 0,
rank: 1,
keywords: ["new", "workspace", "worktree", "branch"],
visibility: "always",
run: () => {
keyboardActionDispatcher.dispatch({ id: "workspace.new", scope: "sidebar" });
},
presentation: {
kind: "action",
title: t("sidebar.actions.newWorkspace"),
sectionTitle: t("shell.commandCenter.actions"),
icon: PlusIcon,
shortcutKeys:
resolveShortcutKeysForAction("new-workspace", overrides, shortcutPlatform) ?? undefined,
},
},
{
id: "home",
group: "actions",
groupRank: 0,
rank: 2,
keywords: ["home", "start", "import", "session", "pair", "device", "providers"],
visibility: "query",
run: () => {
clearCommandCenterFocusRestoreElement();
router.push(homeRoute);
},
presentation: {
kind: "action",
title: t("shell.commandCenter.home"),
sectionTitle: t("shell.commandCenter.actions"),
icon: HomeIcon,
},
},
{
id: "history",
group: "actions",
groupRank: 0,
rank: 3,
keywords: ["history", "sessions", "recent"],
visibility: "always",
run: () => {
clearCommandCenterFocusRestoreElement();
router.push(sessionsRoute);
},
presentation: {
kind: "action",
title: t("sidebar.sections.sessions"),
sectionTitle: t("shell.commandCenter.actions"),
icon: HistoryIcon,
},
},
{
id: "schedules",
group: "actions",
groupRank: 0,
rank: 4,
keywords: ["schedules", "scheduled", "automation", "recurring"],
visibility: "always",
run: () => {
clearCommandCenterFocusRestoreElement();
router.push(schedulesRoute);
},
presentation: {
kind: "action",
title: t("sidebar.sections.schedules"),
sectionTitle: t("shell.commandCenter.actions"),
icon: SchedulesIcon,
},
},
{
id: "settings",
group: "actions",
groupRank: 0,
rank: 5,
keywords: ["settings", "preferences", "config", "configuration"],
visibility: "always",
run: () => {
clearCommandCenterFocusRestoreElement();
router.push(settingsRoute);
},
presentation: {
kind: "action",
title: t("sidebar.actions.settings"),
sectionTitle: t("shell.commandCenter.actions"),
icon: SettingsIcon,
shortcutKeys:
resolveShortcutKeysForAction("toggle-settings", overrides, shortcutPlatform) ??
undefined,
},
},
];
if (shortcutsAvailable) {
availableActions.push({
id: "keyboard-shortcuts",
group: "actions",
groupRank: 0,
rank: 6,
keywords: ["keyboard", "shortcuts", "keys", "hotkeys"],
visibility: "always",
run: () => setShortcutsDialogOpen(true),
presentation: {
kind: "action",
title: t("sidebar.help.shortcuts"),
sectionTitle: t("shell.commandCenter.actions"),
icon: KeyboardIcon,
shortcutKeys:
resolveShortcutKeysForAction("show-shortcuts", overrides, shortcutPlatform) ??
undefined,
},
});
}
availableActions.push(
buildGroupingContribution({
groupMode,
labels: {
section: t("shell.commandCenter.actions"),
groupByProject: t("shell.commandCenter.groupByProject"),
groupByStatus: t("shell.commandCenter.groupByStatus"),
},
icons: { project: FolderIcon, status: CircleDashedIcon },
setGroupMode,
}),
);
return availableActions;
}, [
groupMode,
homeRoute,
openAddProject,
overrides,
schedulesRoute,
sessionsRoute,
setGroupMode,
setShortcutsDialogOpen,
settingsRoute,
shortcutPlatform,
shortcutsAvailable,
t,
]);
useCommandCenterActions({ sourceId: "root", enabled: true, actions });
return null;
}