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,
- },
}));