From cac71ee8f552550eb840ee304e48b874d234b908 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 10 Jul 2026 00:42:36 +0200 Subject: [PATCH] Hide keyboard shortcut badges in workspace menu on native (#1964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Hide keyboard shortcut badges in workspace menu on native The archive item's shortcut badge (e.g. Ctrl+Shift+⌫) rendered in the workspace kebab/context menu on mobile, where there's no physical keyboard to invoke it. Gate the trailing badge behind !isNative in both kebab menu variants. Co-Authored-By: Claude Opus 4.8 * fix(app): hide native shortcuts in every workspace menu Workspace menus were duplicated across project and status views, so the status path missed the native gate. Centralize the menu so every sidebar view shares the same shortcut policy. --------- Co-authored-by: Claude Opus 4.8 --- .../src/components/sidebar-workspace-list.tsx | 122 +------------- .../sidebar/sidebar-status-list.tsx | 134 +--------------- .../sidebar/sidebar-workspace-menu.tsx | 151 ++++++++++++++++++ .../sidebar/sidebar-workspace-row.tsx | 148 +---------------- 4 files changed, 160 insertions(+), 395 deletions(-) create mode 100644 packages/app/src/components/sidebar/sidebar-workspace-menu.tsx diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index d7611099e..fbcac3ff7 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -38,17 +38,13 @@ import { type GestureType } from "react-native-gesture-handler"; import * as Clipboard from "expo-clipboard"; import { DiffStat } from "@/components/diff-stat"; import { - Archive, CircleAlert, - CircleCheck, ChevronDown, ChevronRight, - Copy, ExternalLink, GitPullRequest, Settings, MoreVertical, - Pencil, Plus, Trash2, } from "lucide-react-native"; @@ -93,6 +89,7 @@ import { isEmphasizedStatusDotBucket } from "@/utils/status-dot-color"; import type { SidebarStateBucket } from "@/utils/sidebar-agent-state"; import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-list"; import type { StatusGroup } from "@/hooks/sidebar-status-view-model"; +import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu"; import { SidebarWorkspaceRowFrame, SidebarWorkspaceRowContent, @@ -142,15 +139,11 @@ const ThemedExternalLink = withUnistyles(ExternalLink); const ThemedGitPullRequest = withUnistyles(GitPullRequest); const ThemedActivityIndicator = withUnistyles(ActivityIndicator); const ThemedCircleAlert = withUnistyles(CircleAlert); -const ThemedCircleCheck = withUnistyles(CircleCheck); const ThemedSyncedLoader = withUnistyles(SyncedLoader); const ThemedPlus = withUnistyles(Plus); const ThemedMoreVertical = withUnistyles(MoreVertical); const ThemedTrash2 = withUnistyles(Trash2); const ThemedSettings = withUnistyles(Settings); -const ThemedCopy = withUnistyles(Copy); -const ThemedArchive = withUnistyles(Archive); -const ThemedPencil = withUnistyles(Pencil); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground, @@ -358,12 +351,6 @@ function projectKebabStyle({ return [styles.projectKebabButton, hovered && styles.projectKebabButtonHovered]; } -function workspaceKebabStyle({ - hovered = false, -}: PressableStateCallbackType & { hovered?: boolean }) { - return [styles.kebabButton, hovered && styles.kebabButtonHovered]; -} - function getProjectWorkspaceRowStyle({ isDragging, selected, @@ -540,12 +527,6 @@ function ProjectRowTrailingActions({ const trash2LeadingIcon = ; const settingsLeadingIcon = ; -const copyLeadingIcon = ; -const markAsReadLeadingIcon = ( - -); -const archiveLeadingIcon = ; -const renameLeadingIcon = ; const openInNewWindowLeadingIcon = ( ); @@ -693,7 +674,7 @@ function WorkspaceRowRightGroup({ {onArchive ? ( - void; - onCopyBranchName?: () => void; - onRename?: () => void; - onMarkAsRead?: () => void; - onArchive: () => void; - archiveLabel?: string; - archiveStatus?: "idle" | "pending" | "success"; - archivePendingLabel?: string; - archiveShortcutKeys?: ShortcutKey[][] | null; -}) { - const { t } = useTranslation(); - const archiveTrailing = useMemo( - () => (archiveShortcutKeys ? : null), - [archiveShortcutKeys], - ); - return ( - - - {renderKebabTriggerIcon} - - - {onCopyPath ? ( - - {t("sidebar.workspace.actions.copyPath")} - - ) : null} - {onCopyBranchName ? ( - - {t("sidebar.workspace.actions.copyBranchName")} - - ) : null} - {onRename ? ( - - {t("sidebar.workspace.actions.rename")} - - ) : null} - {onMarkAsRead ? ( - - Mark as read - - ) : null} - - {archiveLabel ?? t("sidebar.workspace.actions.archive")} - - - - ); -} - function ProjectIcon({ iconDataUri, placeholderInitial, @@ -2872,14 +2762,6 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, flexShrink: 0, }, - kebabButton: { - padding: 2, - borderRadius: 4, - marginLeft: 2, - }, - kebabButtonHovered: { - backgroundColor: theme.colors.surface2, - }, statusDotNeedsInput: { backgroundColor: theme.colors.palette.amber[500], borderColor: theme.colors.surface0, diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx index 693dc0ebe..4db918332 100644 --- a/packages/app/src/components/sidebar/sidebar-status-list.tsx +++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx @@ -21,18 +21,8 @@ import { CircleCheck, CircleDot, CircleX, - MoreVertical, - Copy, - Archive, - Pencil, } from "lucide-react-native"; import { DiffStat } from "@/components/diff-stat"; -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, -} from "@/components/ui/dropdown-menu"; import { useToast } from "@/contexts/toast-context"; import { useMutation } from "@tanstack/react-query"; import { getHostRuntimeStore } from "@/runtime/host-runtime"; @@ -43,7 +33,6 @@ import { useWorkspaceArchive } from "@/workspace/use-workspace-archive"; import { useCheckoutGitActionsStore } from "@/git/actions-store"; import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning"; import * as Clipboard from "expo-clipboard"; -import { Shortcut } from "@/components/ui/shortcut"; import type { ShortcutKey } from "@/utils/format-shortcut"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; @@ -56,9 +45,9 @@ import { SidebarWorkspaceTrailingActionSlot, } from "@/components/sidebar/sidebar-workspace-row-content"; import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store"; +import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu"; // Themed icon wrappers -const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); @@ -73,18 +62,6 @@ const ThemedCircleAlert = withUnistyles(CircleAlert); const ThemedCircleCheck = withUnistyles(CircleCheck); const ThemedCircleDot = withUnistyles(CircleDot); const ThemedCircleX = withUnistyles(CircleX); -const ThemedMoreVertical = withUnistyles(MoreVertical); -const ThemedCopy = withUnistyles(Copy); -const ThemedArchive = withUnistyles(Archive); -const ThemedPencil = withUnistyles(Pencil); - -const copyLeadingIcon = ; -const markAsReadLeadingIcon = ( - -); -const archiveLeadingIcon = ; -const renameLeadingIcon = ; - interface StatusWorkspaceListProps { groups: StatusGroup[]; projectNamesByKey: Map; @@ -641,7 +618,7 @@ function StatusWorkspaceActionSlot({ {onArchive ? ( - void; - onCopyBranchName?: () => void; - onRename?: () => void; - onMarkAsRead?: () => void; - onArchive: () => void; - archiveLabel?: string; - archiveStatus?: "idle" | "pending" | "success"; - archivePendingLabel?: string; - archiveShortcutKeys?: ShortcutKey[][] | null; -}) { - const archiveTrailing = useMemo( - () => (archiveShortcutKeys ? : null), - [archiveShortcutKeys], - ); - return ( - - - {({ hovered }: { hovered?: boolean }) => ( - - )} - - - {onCopyPath ? ( - - Copy path - - ) : null} - {onCopyBranchName ? ( - - Copy branch name - - ) : null} - {onRename ? ( - - Rename workspace - - ) : null} - {onMarkAsRead ? ( - - Mark as read - - ) : null} - - {archiveLabel ?? "Archive"} - - - - ); -} - -function kebabStyle({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) { - return [styles.kebabButton, hovered && styles.kebabButtonHovered]; -} - function getStatusWorkspaceRowStyle({ selected, isHovered, @@ -860,12 +738,4 @@ const styles = StyleSheet.create((theme) => ({ sidebarRowSelected: { backgroundColor: theme.colors.surfaceSidebarHover, }, - kebabButton: { - padding: 2, - borderRadius: 4, - marginLeft: 2, - }, - kebabButtonHovered: { - backgroundColor: theme.colors.surface2, - }, })); diff --git a/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx new file mode 100644 index 000000000..5e16ea688 --- /dev/null +++ b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx @@ -0,0 +1,151 @@ +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { type PressableStateCallbackType } from "react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { Archive, CircleCheck, Copy, MoreVertical, Pencil } from "lucide-react-native"; +import { isNative, isWeb } from "@/constants/platform"; +import type { Theme } from "@/styles/theme"; +import type { ShortcutKey } from "@/utils/format-shortcut"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Shortcut } from "@/components/ui/shortcut"; + +const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); +const foregroundMutedColorMapping = (theme: Theme) => ({ + color: theme.colors.foregroundMuted, +}); + +const ThemedMoreVertical = withUnistyles(MoreVertical); +const ThemedCopy = withUnistyles(Copy); +const ThemedArchive = withUnistyles(Archive); +const ThemedPencil = withUnistyles(Pencil); +const ThemedCircleCheck = withUnistyles(CircleCheck); + +const copyLeadingIcon = ; +const renameLeadingIcon = ; +const markAsReadLeadingIcon = ( + +); +const archiveLeadingIcon = ; + +function renderTriggerIcon({ hovered }: { hovered?: boolean }) { + return ( + + ); +} + +interface SidebarWorkspaceMenuProps { + workspaceKey: string; + onCopyPath?: () => void; + onCopyBranchName?: () => void; + onRename?: () => void; + onMarkAsRead?: () => void; + onArchive: () => void; + archiveLabel?: string; + archiveStatus?: "idle" | "pending" | "success"; + archivePendingLabel?: string; + archiveShortcutKeys?: ShortcutKey[][] | null; +} + +export function SidebarWorkspaceMenu({ + workspaceKey, + onCopyPath, + onCopyBranchName, + onRename, + onMarkAsRead, + onArchive, + archiveLabel, + archiveStatus, + archivePendingLabel, + archiveShortcutKeys, +}: SidebarWorkspaceMenuProps) { + const { t } = useTranslation(); + const archiveTrailing = useMemo( + () => (archiveShortcutKeys && !isNative ? : null), + [archiveShortcutKeys], + ); + + return ( + + + {renderTriggerIcon} + + + {onCopyPath ? ( + + {t("sidebar.workspace.actions.copyPath")} + + ) : null} + {onCopyBranchName ? ( + + {t("sidebar.workspace.actions.copyBranchName")} + + ) : null} + {onRename ? ( + + {t("sidebar.workspace.actions.rename")} + + ) : null} + {onMarkAsRead ? ( + + Mark as read + + ) : null} + + {archiveLabel ?? t("sidebar.workspace.actions.archive")} + + + + ); +} + +function triggerStyle({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) { + return [styles.trigger, hovered && styles.triggerHovered]; +} + +const styles = StyleSheet.create((theme) => ({ + trigger: { + padding: 2, + borderRadius: 4, + marginLeft: 2, + }, + triggerHovered: { + backgroundColor: theme.colors.surface2, + }, +})); diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx index 2967d857f..cf8bab866 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx @@ -1,22 +1,13 @@ import { memo, useCallback, useMemo, useState, type Ref } from "react"; import { useTranslation } from "react-i18next"; -import { View, Text, Pressable, type PressableStateCallbackType } from "react-native"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { Archive, CircleCheck, Copy, MoreVertical, Pencil } from "lucide-react-native"; +import { View, Text, Pressable } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; import { useMutation } from "@tanstack/react-query"; import * as Clipboard from "expo-clipboard"; -import type { Theme } from "@/styles/theme"; import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list"; import type { DraggableListDragHandleProps } from "@/components/draggable-list.types"; import type { ShortcutKey } from "@/utils/format-shortcut"; import { DiffStat } from "@/components/diff-stat"; -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, -} from "@/components/ui/dropdown-menu"; -import { Shortcut } from "@/components/ui/shortcut"; import { AdaptiveRenameModal } from "@/components/rename-modal"; import { useToast } from "@/contexts/toast-context"; import { getHostRuntimeStore } from "@/runtime/host-runtime"; @@ -28,8 +19,9 @@ import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attention"; import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect"; import { requireWorkspaceDirectory, resolveWorkspaceDirectory } from "@/utils/workspace-directory"; -import { isWeb as platformIsWeb, isNative as platformIsNative } from "@/constants/platform"; +import { isNative as platformIsNative } from "@/constants/platform"; import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction"; +import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu"; import { SidebarWorkspaceRowFrame, SidebarWorkspaceRowContent, @@ -38,31 +30,6 @@ import { SidebarWorkspaceTrailingActionSlot, } from "@/components/sidebar/sidebar-workspace-row-content"; -const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); -const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); - -const ThemedMoreVertical = withUnistyles(MoreVertical); -const ThemedCopy = withUnistyles(Copy); -const ThemedArchive = withUnistyles(Archive); -const ThemedPencil = withUnistyles(Pencil); -const ThemedCircleCheck = withUnistyles(CircleCheck); - -const copyLeadingIcon = ; -const renameLeadingIcon = ; -const markAsReadLeadingIcon = ( - -); -const archiveLeadingIcon = ; - -function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }) { - return ( - - ); -} - function noop() {} interface SidebarWorkspaceRowProps { @@ -458,7 +425,7 @@ function WorkspaceRowTrailingActions({ {onArchive ? ( - void; - onCopyBranchName?: () => void; - onRename?: () => void; - onMarkAsRead?: () => void; - onArchive: () => void; - archiveLabel?: string; - archiveStatus?: "idle" | "pending" | "success"; - archivePendingLabel?: string; - archiveShortcutKeys?: ShortcutKey[][] | null; -}) { - const { t } = useTranslation(); - const archiveTrailing = useMemo( - () => (archiveShortcutKeys ? : null), - [archiveShortcutKeys], - ); - return ( - - - {renderKebabTriggerIcon} - - - {onCopyPath ? ( - - {t("sidebar.workspace.actions.copyPath")} - - ) : null} - {onCopyBranchName ? ( - - {t("sidebar.workspace.actions.copyBranchName")} - - ) : null} - {onRename ? ( - - {t("sidebar.workspace.actions.rename")} - - ) : null} - {onMarkAsRead ? ( - - Mark as read - - ) : null} - - {archiveLabel ?? t("sidebar.workspace.actions.archive")} - - - - ); -} - -function workspaceKebabStyle({ - hovered = false, -}: PressableStateCallbackType & { hovered?: boolean }) { - return [styles.kebabButton, hovered && styles.kebabButtonHovered]; -} - function getWorkspaceRowStyle({ isDragging, selected, @@ -630,12 +500,4 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, flexShrink: 0, }, - kebabButton: { - padding: 2, - borderRadius: 4, - marginLeft: 2, - }, - kebabButtonHovered: { - backgroundColor: theme.colors.surface2, - }, }));