Make the command center stable and workspace-aware (#2749)

* feat(app): expand command center actions

Keep command center navigation stable by revealing only clipped rows without smooth centering. Prioritize a concise default list while making workspace, Git, pane, and creation actions searchable through the existing contribution and action-policy paths.

* test(app): search for query-only command action

Add project is intentionally absent from the unfiltered command center. Exercise the overlay flow through search before selecting it.
This commit is contained in:
Mohamed Boudra
2026-08-01 15:38:07 +02:00
committed by GitHub
parent 47dcb64ac7
commit fb9f7fb98a
21 changed files with 1178 additions and 247 deletions
+31
View File
@@ -0,0 +1,31 @@
import { withUnistyles } from "react-native-unistyles";
import {
Archive,
ArrowDownUp,
Download,
GitCommitHorizontal,
GitMerge,
RefreshCcw,
Upload,
} from "lucide-react-native";
import type { Theme } from "@/styles/theme";
const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal);
const ThemedDownload = withUnistyles(Download);
const ThemedUpload = withUnistyles(Upload);
const ThemedArrowDownUp = withUnistyles(ArrowDownUp);
const ThemedGitMerge = withUnistyles(GitMerge);
const ThemedRefreshCcw = withUnistyles(RefreshCcw);
const ThemedArchive = withUnistyles(Archive);
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
export const GIT_ACTION_ICONS = {
commit: <ThemedGitCommitHorizontal size={16} uniProps={mutedColorMapping} />,
pull: <ThemedDownload size={16} uniProps={mutedColorMapping} />,
push: <ThemedUpload size={16} uniProps={mutedColorMapping} />,
pullAndPush: <ThemedArrowDownUp size={16} uniProps={mutedColorMapping} />,
merge: <ThemedGitMerge size={16} uniProps={mutedColorMapping} />,
mergeFromBase: <ThemedRefreshCcw size={16} uniProps={mutedColorMapping} />,
archive: <ThemedArchive size={16} uniProps={mutedColorMapping} />,
};
+7 -21
View File
@@ -2,7 +2,7 @@ import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { useCallback, useMemo } from "react";
import { View, Text, Pressable, type PressableStateCallbackType } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { ChevronDown, Info, MoreVertical } from "lucide-react-native";
import { ChevronDown, MoreVertical } from "lucide-react-native";
import { useTranslation } from "react-i18next";
import {
DropdownMenu,
@@ -15,8 +15,8 @@ import { Shortcut } from "@/components/ui/shortcut";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { useToast } from "@/contexts/toast-context";
import type { GitAction, GitActions } from "@/git/policy";
import { useGitActionRunner } from "@/git/use-actions";
interface GitActionsSplitButtonProps {
gitActions: GitActions;
@@ -76,7 +76,7 @@ function GitActionMenuItem({
export function GitActionsSplitButton({ gitActions, hideLabels }: GitActionsSplitButtonProps) {
const { theme } = useUnistyles();
const { t } = useTranslation();
const toast = useToast();
const runGitAction = useGitActionRunner();
const archiveShortcutKeys = useShortcutKeys("archive-workspace");
const getActionDisplayLabel = useCallback((action: GitAction): string => {
@@ -85,26 +85,12 @@ export function GitActionsSplitButton({ gitActions, hideLabels }: GitActionsSpli
return action.label;
}, []);
const handleActionSelect = useCallback(
(action: GitAction) => {
if (action.unavailableMessage) {
toast.show(action.unavailableMessage, {
durationMs: 3200,
icon: <Info size={16} color={theme.colors.foreground} />,
});
return;
}
action.handler();
},
[theme.colors.foreground, toast],
);
const handlePrimaryPress = useCallback(() => {
if (!gitActions.primary) {
return;
}
handleActionSelect(gitActions.primary);
}, [gitActions.primary, handleActionSelect]);
runGitAction(gitActions.primary);
}, [gitActions.primary, runGitAction]);
const overflowMenuButtonStyle = useMemo(() => [styles.iconButton, styles.overflowMenuButton], []);
@@ -172,7 +158,7 @@ export function GitActionsSplitButton({ gitActions, hideLabels }: GitActionsSpli
<GitActionMenuItem
key={action.id}
action={action}
onSelect={handleActionSelect}
onSelect={runGitAction}
archiveShortcutKeys={archiveShortcutKeys}
needsSeparator={action.startsGroup}
showSeparator={index > 0}
@@ -205,7 +191,7 @@ export function GitActionsSplitButton({ gitActions, hideLabels }: GitActionsSpli
<GitActionMenuItem
key={action.id}
action={action}
onSelect={handleActionSelect}
onSelect={runGitAction}
closeOnSelect={false}
/>
))}
+2 -27
View File
@@ -30,22 +30,15 @@ import { BORDER_WIDTH, ICON_SIZE, SPACING, type Theme } from "@/styles/theme";
import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
import {
AlignJustify,
Archive,
ArrowDownUp,
ChevronDown,
Columns2,
Download,
FolderTree,
GitCommitHorizontal,
GitMerge,
List,
ListChevronsDownUp,
ListChevronsUpDown,
Maximize2,
Pilcrow,
RefreshCcw,
RotateCw,
Upload,
WrapText,
} from "lucide-react-native";
import { type ParsedDiffFile, type DiffLine, type HighlightToken } from "@/git/use-diff-query";
@@ -90,6 +83,7 @@ import { lineNumberGutterWidth } from "@/components/code-insets";
import { GitActionsSplitButton } from "@/git/actions-split-button";
import { BranchSwitcher } from "@/components/branch-switcher";
import { useGitActions } from "@/git/use-actions";
import { GIT_ACTION_ICONS } from "@/git/action-icons";
import { buildForgeSignInCommand, getForgePresentation, type Forge } from "@/git/forge";
import { parseGitRemoteLocation } from "@getpaseo/protocol/git-remote";
import type { ForgeAuthState } from "@getpaseo/protocol/messages";
@@ -1330,13 +1324,6 @@ const ThemedListChevronsUpDown = withUnistyles(ListChevronsUpDown);
const ThemedFolderTree = withUnistyles(FolderTree);
const ThemedList = withUnistyles(List);
const ThemedMaximize2 = withUnistyles(Maximize2);
const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal);
const ThemedDownload = withUnistyles(Download);
const ThemedUpload = withUnistyles(Upload);
const ThemedArrowDownUp = withUnistyles(ArrowDownUp);
const ThemedGitMerge = withUnistyles(GitMerge);
const ThemedRefreshCcw = withUnistyles(RefreshCcw);
const ThemedArchive = withUnistyles(Archive);
const ThemedChevronDown = withUnistyles(ChevronDown);
const DIFF_OPTIONS_WHITESPACE_ICON = (
<ThemedPilcrow size={14} uniProps={foregroundMutedIconColorMapping} />
@@ -2823,22 +2810,10 @@ export function GitDiffPane({
() => computeBaseRefLabel(baseRef, t("workspace.git.diff.base")),
[baseRef, t],
);
const gitActionsIcons = useMemo(
() => ({
commit: <ThemedGitCommitHorizontal size={16} uniProps={foregroundMutedIconColorMapping} />,
pull: <ThemedDownload size={16} uniProps={foregroundMutedIconColorMapping} />,
push: <ThemedUpload size={16} uniProps={foregroundMutedIconColorMapping} />,
pullAndPush: <ThemedArrowDownUp size={16} uniProps={foregroundMutedIconColorMapping} />,
merge: <ThemedGitMerge size={16} uniProps={foregroundMutedIconColorMapping} />,
mergeFromBase: <ThemedRefreshCcw size={16} uniProps={foregroundMutedIconColorMapping} />,
archive: <ThemedArchive size={16} uniProps={foregroundMutedIconColorMapping} />,
}),
[],
);
const { gitActions, branchLabel } = useGitActions({
serverId,
cwd,
icons: gitActionsIcons,
icons: GIT_ACTION_ICONS,
});
const committedDiffDescription = useMemo(
() => computeCommittedDiffDescription(branchLabel, baseRefLabel),
+22
View File
@@ -1,4 +1,6 @@
import { useState, useCallback, useEffect, useMemo, type ReactElement } from "react";
import { Info } from "lucide-react-native";
import { withUnistyles } from "react-native-unistyles";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useTranslation } from "react-i18next";
import type { Theme } from "@/styles/theme";
@@ -31,6 +33,26 @@ import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity";
export type { GitActionId, GitAction, GitActions } from "@/git/policy";
const forgeMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
const ThemedInfo = withUnistyles(Info, (theme) => ({ color: theme.colors.foreground }));
export function useGitActionRunner(): (action: GitAction) => void {
const toast = useToast();
return useCallback(
(action: GitAction) => {
if (action.disabled) return;
if (action.unavailableMessage) {
toast.show(action.unavailableMessage, {
durationMs: 3200,
icon: <ThemedInfo size={16} />,
});
return;
}
action.handler();
},
[toast],
);
}
/**
* The leading icon for every change-request action (create/view/merge) is the
+2 -34
View File
@@ -1,16 +1,6 @@
import { withUnistyles } from "react-native-unistyles";
import {
Archive,
ArrowDownUp,
Download,
GitCommitHorizontal,
GitMerge,
RefreshCcw,
Upload,
} from "lucide-react-native";
import { GitActionsSplitButton } from "@/git/actions-split-button";
import { useGitActions } from "@/git/use-actions";
import type { Theme } from "@/styles/theme";
import { GIT_ACTION_ICONS } from "@/git/action-icons";
interface WorkspaceActionsProps {
serverId: string;
@@ -18,33 +8,11 @@ interface WorkspaceActionsProps {
hideLabels?: boolean;
}
const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal);
const ThemedDownload = withUnistyles(Download);
const ThemedUpload = withUnistyles(Upload);
const ThemedArrowDownUp = withUnistyles(ArrowDownUp);
const ThemedGitMerge = withUnistyles(GitMerge);
const ThemedRefreshCcw = withUnistyles(RefreshCcw);
const ThemedArchive = withUnistyles(Archive);
const mutedColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundMuted,
});
const ICONS = {
commit: <ThemedGitCommitHorizontal size={16} uniProps={mutedColorMapping} />,
pull: <ThemedDownload size={16} uniProps={mutedColorMapping} />,
push: <ThemedUpload size={16} uniProps={mutedColorMapping} />,
pullAndPush: <ThemedArrowDownUp size={16} uniProps={mutedColorMapping} />,
merge: <ThemedGitMerge size={16} uniProps={mutedColorMapping} />,
mergeFromBase: <ThemedRefreshCcw size={16} uniProps={mutedColorMapping} />,
archive: <ThemedArchive size={16} uniProps={mutedColorMapping} />,
};
export function WorkspaceActions({ serverId, cwd, hideLabels }: WorkspaceActionsProps) {
const { gitActions } = useGitActions({
serverId,
cwd,
icons: ICONS,
icons: GIT_ACTION_ICONS,
});
return <GitActionsSplitButton gitActions={gitActions} hideLabels={hideLabels} />;