Always show workspace hover card with branch and directory

The hover card was previously gated to open only when a PR hint or diff
stat was available. It now opens on every desktop-web workspace hover so
users can see the workspace name, branch, and working directory. The CWD
is shortened using the existing shortenPath helper (/Users/... -> ~).
This commit is contained in:
Mohamed Boudra
2026-06-16 15:27:02 +07:00
parent f5d39446cc
commit e77c8e2bf3
2 changed files with 56 additions and 24 deletions

View File

@@ -12,7 +12,14 @@ import { Dimensions, Text, View } from "react-native";
import { useTranslation } from "react-i18next";
import { FadeIn, FadeOut } from "react-native-reanimated";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { CircleCheck, CircleDot, CircleX, ExternalLink, GitBranch } from "lucide-react-native";
import {
CircleCheck,
CircleDot,
CircleX,
ExternalLink,
Folder,
GitBranch,
} from "lucide-react-native";
import { GitHubIcon } from "@/components/icons/github-icon";
import type { Theme } from "@/styles/theme";
import { DiffStat } from "@/components/diff-stat";
@@ -22,6 +29,7 @@ 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 { PrBadge } from "@/components/sidebar-workspace-list";
import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone";
import { useIsCompactFormFactor } from "@/constants/layout";
@@ -113,8 +121,6 @@ function WorkspaceHoverCardDesktop({
const graceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const triggerHoveredRef = useRef(false);
const hasContent = prHint !== null || !!workspace.diffStat;
const clearGraceTimer = useCallback(() => {
if (graceTimerRef.current) {
clearTimeout(graceTimerRef.current);
@@ -133,10 +139,10 @@ function WorkspaceHoverCardDesktop({
const handleTriggerEnter = useCallback(() => {
triggerHoveredRef.current = true;
clearGraceTimer();
if (!isDragging && hasContent) {
if (!isDragging) {
setOpen(true);
}
}, [clearGraceTimer, isDragging, hasContent]);
}, [clearGraceTimer, isDragging]);
const handleTriggerLeave = useCallback(() => {
triggerHoveredRef.current = false;
@@ -162,19 +168,6 @@ function WorkspaceHoverCardDesktop({
}
}, [isDragging, clearGraceTimer]);
// When content becomes available while trigger is already hovered, open the card.
useEffect(() => {
if (!hasContent) {
clearGraceTimer();
setOpen(false);
return;
}
if (isDragging) return;
if (triggerHoveredRef.current) {
setOpen(true);
}
}, [clearGraceTimer, hasContent, isDragging]);
// Cleanup on unmount
useEffect(() => {
return () => {
@@ -190,7 +183,7 @@ function WorkspaceHoverCardDesktop({
onPointerLeave={handleTriggerLeave}
>
{children}
{open && hasContent ? (
{open ? (
<WorkspaceHoverCardContent
workspace={workspace}
prHint={prHint}
@@ -214,6 +207,7 @@ function WorkspaceHoverCardContent({
contentRef: React.RefObject<View | null>;
}): ReactElement | null {
const { t } = useTranslation();
const cwdDisplay = shortenPath(workspace.workspaceDirectory);
const bottomSheetInternal = useBottomSheetModalInternal(true);
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
@@ -286,11 +280,11 @@ function WorkspaceHoverCardContent({
{workspace.name}
</Text>
</View>
{workspace.currentBranch && workspace.currentBranch !== workspace.name ? (
<View style={styles.cardBranchRow}>
{workspace.currentBranch ? (
<View style={styles.cardInfoRow}>
<ThemedGitBranch size={12} uniProps={foregroundMutedColorMapping} />
<Text
style={styles.cardBranchText}
style={styles.cardInfoText}
numberOfLines={1}
testID="hover-card-workspace-branch"
>
@@ -298,6 +292,14 @@ function WorkspaceHoverCardContent({
</Text>
</View>
) : null}
{cwdDisplay ? (
<View style={styles.cardInfoRow}>
<ThemedFolder size={12} uniProps={foregroundMutedColorMapping} />
<Text style={styles.cardInfoText} numberOfLines={1} testID="hover-card-workspace-cwd">
{cwdDisplay}
</Text>
</View>
) : null}
{prHint || workspace.diffStat ? (
<View style={styles.cardMetaRow}>
{workspace.diffStat ? (
@@ -322,6 +324,7 @@ function WorkspaceHoverCardContent({
}
const ThemedGitBranch = withUnistyles(GitBranch);
const ThemedFolder = withUnistyles(Folder);
const ThemedExternalLink = withUnistyles(ExternalLink);
const ThemedGitHubIcon = withUnistyles(GitHubIcon);
const ThemedCircleCheck = withUnistyles(CircleCheck);
@@ -484,14 +487,14 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[3],
paddingBottom: theme.spacing[2],
},
cardBranchRow: {
cardInfoRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1.5],
paddingHorizontal: theme.spacing[3],
paddingBottom: theme.spacing[2],
},
cardBranchText: {
cardInfoText: {
flex: 1,
minWidth: 0,
color: theme.colors.foregroundMuted,

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { shortenPath } from "./shorten-path";
describe("shortenPath", () => {
it("shortens a macOS home directory path", () => {
expect(shortenPath("/Users/moboudra/dev/paseo")).toBe("~/dev/paseo");
});
it("shortens a Linux home directory path", () => {
expect(shortenPath("/home/moboudra/dev/paseo")).toBe("~/dev/paseo");
});
it("leaves non-home absolute paths unchanged", () => {
expect(shortenPath("/var/www/app")).toBe("/var/www/app");
});
it("leaves Windows paths unchanged", () => {
expect(shortenPath("C:\\Users\\moboudra\\dev\\paseo")).toBe("C:\\Users\\moboudra\\dev\\paseo");
});
it("returns an empty string for null or undefined", () => {
expect(shortenPath(null)).toBe("");
expect(shortenPath(undefined)).toBe("");
});
it("returns an empty string for an empty string", () => {
expect(shortenPath("")).toBe("");
});
});