chore: checkpoint current worktree state

This commit is contained in:
Mohamed Boudra
2026-04-11 13:09:51 +07:00
parent 3b8cfcbec6
commit aa1d33f0a0
7 changed files with 246 additions and 301 deletions
@@ -6,11 +6,10 @@ import {
type PropsWithChildren,
type ReactElement,
} from "react";
import { useMutation } from "@tanstack/react-query";
import { Dimensions, Platform, Text, View } from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { ExternalLink, Globe, LoaderCircle, Play, SquareTerminal } from "lucide-react-native";
import { ExternalLink } from "lucide-react-native";
import { GitHubIcon } from "@/components/icons/github-icon";
import { DiffStat } from "@/components/diff-stat";
import { Pressable } from "react-native";
@@ -18,10 +17,7 @@ import { Portal } from "@gorhom/portal";
import { useBottomSheetModalInternal } from "@gorhom/bottom-sheet";
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
import type { PrHint } from "@/hooks/use-checkout-pr-status-query";
import { useToast } from "@/contexts/toast-context";
import { useSessionStore } from "@/stores/session-store";
import { openExternalUrl } from "@/utils/open-external-url";
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
import { PrBadge } from "@/components/sidebar-workspace-list";
interface Rect {
@@ -108,8 +104,7 @@ function WorkspaceHoverCardDesktop({
const triggerHoveredRef = useRef(false);
const contentHoveredRef = useRef(false);
const hasScripts = workspace.scripts.length > 0;
const hasContent = hasScripts || prHint !== null;
const hasContent = prHint !== null || !!workspace.diffStat;
const clearGraceTimer = useCallback(() => {
if (graceTimerRef.current) {
@@ -195,32 +190,6 @@ function WorkspaceHoverCardDesktop({
);
}
function getScriptHealthColor(input: {
health: SidebarWorkspaceEntry["scripts"][number]["health"];
theme: ReturnType<typeof useUnistyles>["theme"];
}): string {
if (input.health === "healthy") {
return input.theme.colors.palette.blue[500];
}
if (input.health === "unhealthy") {
return input.theme.colors.palette.red[500];
}
return input.theme.colors.foregroundMuted;
}
function getScriptHealthLabel(
health: SidebarWorkspaceEntry["scripts"][number]["health"],
): "Healthy" | "Unhealthy" | "Unknown" {
if (health === "healthy") {
return "Healthy";
}
if (health === "unhealthy") {
return "Unhealthy";
}
return "Unknown";
}
function WorkspaceHoverCardContent({
workspace,
prHint,
@@ -235,39 +204,10 @@ function WorkspaceHoverCardContent({
onContentLeave: () => void;
}): ReactElement | null {
const { theme } = useUnistyles();
const toast = useToast();
const client = useSessionStore((state) => state.sessions[workspace.serverId]?.client ?? null);
const bottomSheetInternal = useBottomSheetModalInternal(true);
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
const startScriptMutation = useMutation({
mutationFn: async (scriptName: string) => {
if (!client) {
throw new Error("Daemon client not available");
}
const result = await client.startWorkspaceScript(workspace.workspaceId, scriptName);
if (result.error) {
throw new Error(result.error);
}
return result;
},
onSuccess: (data) => {
if (data.terminalId) {
navigateToPreparedWorkspaceTab({
serverId: workspace.serverId,
workspaceId: workspace.workspaceId,
target: { kind: "terminal", terminalId: data.terminalId },
});
}
},
onError: (error, scriptName) => {
toast.show(
error instanceof Error ? error.message : `Failed to start ${scriptName}`,
{ variant: "error" },
);
},
});
// Measure trigger — same pattern as tooltip.tsx
useEffect(() => {
@@ -345,167 +285,6 @@ function WorkspaceHoverCardContent({
{prHint ? <PrBadge hint={prHint} /> : null}
</View>
) : null}
{workspace.scripts.length > 0 ? (
<>
<View style={styles.separator} />
<View style={styles.sectionLabelRow}>
<Play size={12} color={theme.colors.foregroundMuted} fill="transparent" />
<Text style={styles.sectionLabel}>Scripts</Text>
</View>
<View style={styles.sectionList} testID="hover-card-script-list">
{workspace.scripts.map((script) => {
const isRunning = script.lifecycle === "running";
const isService = (script.type ?? "service") === "service";
const isLinkable = isService && isRunning && !!script.url;
const exitCode = script.exitCode ?? null;
let dotColor: string;
if (isService) {
dotColor = isRunning
? getScriptHealthColor({ health: script.health, theme })
: theme.colors.foregroundMuted;
} else if (isRunning) {
dotColor = theme.colors.palette.blue[500];
} else if (exitCode === 0) {
dotColor = theme.colors.palette.green[500];
} else if (exitCode !== null) {
dotColor = theme.colors.palette.red[500];
} else {
dotColor = theme.colors.foregroundMuted;
}
let accessibilityStatus: string;
if (isService) {
accessibilityStatus = isRunning ? getScriptHealthLabel(script.health) : "Stopped";
} else if (isRunning) {
accessibilityStatus = "Running";
} else if (exitCode === 0) {
accessibilityStatus = "Completed";
} else if (exitCode !== null) {
accessibilityStatus = `Failed (exit ${exitCode})`;
} else {
accessibilityStatus = "Stopped";
}
return (
<Pressable
key={script.hostname}
accessibilityRole={isLinkable ? "link" : undefined}
accessibilityLabel={`${script.scriptName} script — ${accessibilityStatus}`}
testID={`hover-card-script-${script.scriptName}`}
style={({ hovered }) => [
styles.listRow,
hovered && isLinkable && styles.listRowHovered,
]}
onPress={isLinkable ? () => void openExternalUrl(script.url!) : undefined}
disabled={!isLinkable}
>
{({ hovered }) => (
<>
{isService ? (
<Globe
size={14}
color={dotColor}
testID={`hover-card-script-health-${script.scriptName}`}
style={styles.scriptIcon}
/>
) : (
<SquareTerminal
size={14}
color={dotColor}
testID={`hover-card-script-health-${script.scriptName}`}
style={styles.scriptIcon}
/>
)}
<Text
style={[
styles.listRowLabel,
{
color: isRunning
? theme.colors.foreground
: theme.colors.foregroundMuted,
},
]}
numberOfLines={1}
>
{script.scriptName}
</Text>
{isService && isRunning && script.url ? (
<Text style={styles.listRowSecondary} numberOfLines={1}>
{script.url.replace(/^https?:\/\//, "")}
</Text>
) : !isService && !isRunning && exitCode !== null && exitCode !== 0 ? (
<Text style={styles.listRowSecondary} numberOfLines={1}>
exit {exitCode}
</Text>
) : (
<View style={styles.listRowSpacer} />
)}
{isRunning ? (
isLinkable && hovered ? (
<View
style={[
styles.externalLinkOverlay,
{
backgroundImage: `linear-gradient(to right, transparent, ${theme.colors.surface2} 40%)`,
},
]}
>
<ExternalLink
size={12}
color={theme.colors.foreground}
/>
</View>
) : null
) : (
<Pressable
accessibilityRole="button"
accessibilityLabel={`Run ${script.scriptName} script`}
testID={`hover-card-script-start-${script.scriptName}`}
hitSlop={4}
disabled={startScriptMutation.isPending}
onPress={(event) => {
event.stopPropagation();
startScriptMutation.mutate(script.scriptName);
}}
style={styles.startButton}
>
{({ hovered: actionHovered }) =>
startScriptMutation.isPending &&
startScriptMutation.variables === script.scriptName ? (
<LoaderCircle size={12} color={theme.colors.foregroundMuted} />
) : (
<>
<Play
size={10}
color={actionHovered ? theme.colors.foreground : theme.colors.foregroundMuted}
fill="transparent"
/>
<Text
style={[
styles.startButtonLabel,
{
color: actionHovered
? theme.colors.foreground
: theme.colors.foregroundMuted,
},
]}
>
Run
</Text>
</>
)
}
</Pressable>
)}
</>
)}
</Pressable>
);
})}
</View>
</>
) : null}
{prHint?.checks && prHint.checks.length > 0 ? (
<>
<View style={styles.separator} />
@@ -619,30 +398,6 @@ const styles = StyleSheet.create((theme) => ({
height: 1,
backgroundColor: theme.colors.border,
},
sectionLabelRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1.5],
paddingHorizontal: theme.spacing[3],
paddingTop: theme.spacing[2],
paddingBottom: theme.spacing[1],
},
sectionLabel: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted,
},
sectionList: {
paddingBottom: theme.spacing[1],
},
listRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
paddingVertical: 6,
minHeight: 28,
},
listRowHovered: {
backgroundColor: theme.colors.surface2,
},
@@ -656,27 +411,6 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "center",
},
listRowLabel: {
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
lineHeight: 18,
flexShrink: 0,
},
listRowSecondary: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
lineHeight: 18,
flex: 1,
minWidth: 0,
textAlign: "right",
},
listRowSpacer: {
flex: 1,
minWidth: 0,
},
scriptIcon: {
flexShrink: 0,
},
checksSummaryRow: {
flexDirection: "row",
alignItems: "center",
@@ -706,13 +440,4 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
},
startButton: {
flexDirection: "row",
alignItems: "center",
gap: 3,
},
startButtonLabel: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
},
}));