diff --git a/packages/app/src/components/workspace-hover-card.tsx b/packages/app/src/components/workspace-hover-card.tsx
index 0a833dfc6..749cdb6fa 100644
--- a/packages/app/src/components/workspace-hover-card.tsx
+++ b/packages/app/src/components/workspace-hover-card.tsx
@@ -13,9 +13,11 @@ import { useTranslation } from "react-i18next";
import { FadeIn, FadeOut } from "react-native-reanimated";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import {
+ Check,
CircleCheck,
CircleDot,
CircleX,
+ Copy,
ExternalLink,
Folder,
GitBranch,
@@ -24,12 +26,14 @@ import { GitHubIcon } from "@/components/icons/github-icon";
import type { Theme } from "@/styles/theme";
import { DiffStat } from "@/components/diff-stat";
import { Pressable } from "react-native";
+import type { GestureResponderEvent } from "react-native";
import { Portal } from "@gorhom/portal";
import { useBottomSheetModalInternal } from "@gorhom/bottom-sheet";
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
import type { PrHint } from "@/git/use-pr-status-query";
import { openExternalUrl } from "@/utils/open-external-url";
import { shortenPath } from "@/utils/shorten-path";
+import { copyToClipboard } from "@/utils/copy-to-clipboard";
import { PrBadge } from "@/components/sidebar-workspace-list";
import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone";
import { useIsCompactFormFactor } from "@/constants/layout";
@@ -276,29 +280,27 @@ function WorkspaceHoverCardContent({
frameStyle={frameStyle}
>
-
+
{workspace.name}
{workspace.currentBranch ? (
-
-
-
- {workspace.currentBranch}
-
-
+
) : null}
{cwdDisplay ? (
-
-
-
- {cwdDisplay}
-
-
+
) : null}
{prHint || workspace.diffStat ? (
@@ -330,6 +332,8 @@ const ThemedGitHubIcon = withUnistyles(GitHubIcon);
const ThemedCircleCheck = withUnistyles(CircleCheck);
const ThemedCircleDot = withUnistyles(CircleDot);
const ThemedCircleX = withUnistyles(CircleX);
+const ThemedCopy = withUnistyles(Copy);
+const ThemedCheck = withUnistyles(Check);
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
@@ -337,6 +341,70 @@ const successColorMapping = (theme: Theme) => ({ color: theme.colors.statusSucce
const warningColorMapping = (theme: Theme) => ({ color: theme.colors.statusWarning });
const dangerColorMapping = (theme: Theme) => ({ color: theme.colors.statusDanger });
+function CopyableInfoRow({
+ icon: Icon,
+ value,
+ copyValue,
+ copyLabel,
+ testID,
+}: {
+ icon: React.ComponentType>;
+ value: string;
+ copyValue: string;
+ copyLabel: string;
+ testID: string;
+}) {
+ const [isHovered, setIsHovered] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ const handlePressIn = useCallback((event: GestureResponderEvent) => {
+ event.stopPropagation();
+ }, []);
+
+ const handlePress = useCallback(() => {
+ void copyToClipboard(copyValue);
+ setCopied(true);
+ setTimeout(() => {
+ setCopied(false);
+ }, 2000);
+ }, [copyValue]);
+
+ const handleHoverIn = useCallback(() => setIsHovered(true), []);
+ const handleHoverOut = useCallback(() => setIsHovered(false), []);
+
+ let iconUniProps = foregroundMutedColorMapping;
+ if (copied || isHovered) {
+ iconUniProps = foregroundColorMapping;
+ }
+ const textStyle = copied || isHovered ? cardInfoTextHoveredCombined : styles.cardInfoText;
+
+ return (
+
+ {(() => {
+ if (copied) {
+ return ;
+ }
+ if (isHovered) {
+ return ;
+ }
+ return ;
+ })()}
+
+ {value}
+
+
+ );
+}
+
function getChecksSummaryCounts(checks: NonNullable) {
return checks.reduce(
(counts, check) => {
@@ -501,6 +569,9 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
},
+ cardInfoTextHovered: {
+ color: theme.colors.foreground,
+ },
separator: {
height: 1,
backgroundColor: theme.colors.border,
@@ -557,3 +628,5 @@ const checksSummaryLabelHoveredCombined = [
styles.checksSummaryLabel,
styles.checksSummaryLabelHovered,
];
+
+const cardInfoTextHoveredCombined = [styles.cardInfoText, styles.cardInfoTextHovered];
diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts
index 9e0a58043..7999db547 100644
--- a/packages/app/src/i18n/resources/ar.ts
+++ b/packages/app/src/i18n/resources/ar.ts
@@ -337,6 +337,9 @@ export const ar: TranslationResources = {
},
hoverCard: {
scriptsAccessibility: "البرامج النصية Workspace",
+ copyPath: "نسخ المسار",
+ copyBranchName: "نسخ اسم الفرع",
+ copied: "تم النسخ",
},
fileExplorer: {
sort: {
diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts
index 0282b2425..85e30d5b1 100644
--- a/packages/app/src/i18n/resources/en.ts
+++ b/packages/app/src/i18n/resources/en.ts
@@ -336,6 +336,9 @@ export const en = {
},
hoverCard: {
scriptsAccessibility: "Workspace scripts",
+ copyPath: "Copy path",
+ copyBranchName: "Copy branch name",
+ copied: "Copied",
},
fileExplorer: {
sort: {
diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts
index f4978c73c..ce0672e5b 100644
--- a/packages/app/src/i18n/resources/es.ts
+++ b/packages/app/src/i18n/resources/es.ts
@@ -340,6 +340,9 @@ export const es: TranslationResources = {
},
hoverCard: {
scriptsAccessibility: "GuionesWorkspace",
+ copyPath: "Copiar ruta",
+ copyBranchName: "Copiar nombre de rama",
+ copied: "Copiado",
},
fileExplorer: {
sort: {
diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts
index ff8c3383c..6966020db 100644
--- a/packages/app/src/i18n/resources/fr.ts
+++ b/packages/app/src/i18n/resources/fr.ts
@@ -340,6 +340,9 @@ export const fr: TranslationResources = {
},
hoverCard: {
scriptsAccessibility: "ScriptsWorkspace",
+ copyPath: "Copier le chemin",
+ copyBranchName: "Copier le nom de la branche",
+ copied: "Copié",
},
fileExplorer: {
sort: {
diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts
index fcba2642d..e1c9b5d34 100644
--- a/packages/app/src/i18n/resources/ru.ts
+++ b/packages/app/src/i18n/resources/ru.ts
@@ -339,6 +339,9 @@ export const ru: TranslationResources = {
},
hoverCard: {
scriptsAccessibility: "Скрипты Workspace",
+ copyPath: "Копировать путь",
+ copyBranchName: "Копировать имя ветки",
+ copied: "Скопировано",
},
fileExplorer: {
sort: {
diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts
index 98be203d5..cecf2d1a0 100644
--- a/packages/app/src/i18n/resources/zh-CN.ts
+++ b/packages/app/src/i18n/resources/zh-CN.ts
@@ -337,6 +337,9 @@ export const zhCN: TranslationResources = {
},
hoverCard: {
scriptsAccessibility: "Workspace scripts",
+ copyPath: "复制路径",
+ copyBranchName: "复制分支名称",
+ copied: "已复制",
},
fileExplorer: {
sort: {
diff --git a/packages/app/src/utils/copy-to-clipboard.ts b/packages/app/src/utils/copy-to-clipboard.ts
new file mode 100644
index 000000000..2ee7f96e2
--- /dev/null
+++ b/packages/app/src/utils/copy-to-clipboard.ts
@@ -0,0 +1,5 @@
+import * as Clipboard from "expo-clipboard";
+
+export async function copyToClipboard(text: string): Promise {
+ await Clipboard.setStringAsync(text);
+}