import { sanitizeFeatureBranchName } from "@t3tools/shared/git";import { useNavigation, type StaticScreenProps } from "@react-navigation/native";import { useState } from "react";import { Platform, Pressable, ScrollView, View } from "react-native";import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AndroidSheetHeader } from "../../../components/AndroidScreenHeader";import { AppText as Text, AppTextInput as TextInput } from "../../../components/AppText";import { cn } from "../../../lib/cn";import { useEnvironmentQuery } from "../../../state/query";import { useThreadSelection } from "../../../state/use-thread-selection";import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions";import { useSelectedThreadGitState } from "../../../state/use-selected-thread-git-state";import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-worktree";import { vcsEnvironment } from "../../../state/vcs";import { SheetActionButton } from "./gitSheetComponents"; type GitBranchesSheetProps = StaticScreenProps<{ readonly environmentId: string; readonly threadId: string;}>; export function GitBranchesSheet(_props: GitBranchesSheetProps) { const navigation = useNavigation(); const insets = useSafeAreaInsets(); const { selectedThread } = useThreadSelection(); const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); const gitState = useSelectedThreadGitState(); const gitActions = useSelectedThreadGitActions(); const gitStatus = useEnvironmentQuery( selectedThread !== null && selectedThreadCwd !== null ? vcsEnvironment.status({ environmentId: selectedThread.environmentId, input: { cwd: selectedThreadCwd }, }) : null, ); const currentBranchLabel = gitStatus.data?.refName ?? selectedThread?.branch ?? "Detached HEAD"; const currentWorktreePath = selectedThreadWorktreePath; const availableBranches = gitState.selectedThreadBranches; const branchesLoading = gitState.selectedThreadBranchesLoading; const busy = gitState.gitOperationLabel !== null; const [newBranchName, setNewBranchName] = useState(""); const [worktreeBaseBranch, setWorktreeBaseBranch] = useState( currentBranchLabel === "Detached HEAD" ? "main" : currentBranchLabel, ); const [worktreeBranchName, setWorktreeBranchName] = useState(""); const disabledExistingBranchNames: Array<string> = []; for (const branch of availableBranches) { if (branch.worktreePath !== null && branch.worktreePath !== currentWorktreePath) { disabledExistingBranchNames.push(branch.name); } } const disabledExistingBranches = new Set(disabledExistingBranchNames); return ( <View collapsable={false} className="flex-1 bg-sheet"> {Platform.OS === "android" ? ( <AndroidSheetHeader title="Branches & worktrees" onBack={() => navigation.goBack()} /> ) : null} <ScrollView className="flex-1" showsVerticalScrollIndicator={false} contentInset={{ bottom: Math.max(insets.bottom, 18) + 18 }} contentContainerClassName="gap-4 px-5 pt-2" > <View className="gap-2 rounded-[18px] border border-border bg-card px-4 py-4"> <Text className="text-foreground-secondary text-2xs font-t3-bold tracking-[1px] uppercase"> New branch </Text> <TextInput value={newBranchName} onChangeText={setNewBranchName} placeholder="feature/mobile-polish" className="rounded-[18px]" /> <SheetActionButton icon="plus" label="Create & checkout" tone="primary" disabled={busy || newBranchName.trim().length === 0} onPress={() => { const branch = sanitizeFeatureBranchName(newBranchName.trim()); if (branch.length === 0) return; void gitActions.onCreateSelectedThreadBranch(branch).then(() => { setNewBranchName(""); navigation.goBack(); }); }} /> </View> <View className="gap-2 rounded-[18px] border border-border bg-card px-4 py-4"> <Text className="text-foreground-secondary text-2xs font-t3-bold tracking-[1px] uppercase"> New worktree </Text> <TextInput value={worktreeBaseBranch} onChangeText={setWorktreeBaseBranch} placeholder="main" className="rounded-[18px]" /> <TextInput value={worktreeBranchName} onChangeText={setWorktreeBranchName} placeholder="feature/mobile-thread" className="rounded-[18px]" /> <SheetActionButton icon="square.split.2x1" label="Create worktree" tone="primary" disabled={ busy || worktreeBaseBranch.trim().length === 0 || worktreeBranchName.trim().length === 0 } onPress={() => { const baseBranch = worktreeBaseBranch.trim(); const newBranch = worktreeBranchName.trim(); if (baseBranch.length === 0 || newBranch.length === 0) return; void gitActions.onCreateSelectedThreadWorktree({ baseBranch, newBranch }).then(() => { setWorktreeBranchName(""); navigation.goBack(); }); }} /> </View> <View className="gap-2"> <Text className="text-foreground-secondary text-2xs font-t3-bold tracking-[1px] uppercase"> Existing branches </Text> {branchesLoading ? ( <Text className="text-foreground-secondary text-sm font-medium"> Loading branches... </Text> ) : null} {!branchesLoading && availableBranches.length === 0 ? ( <Text className="text-foreground-secondary text-sm font-medium"> No local branches found. </Text> ) : null} {availableBranches.map((branch) => { const disabled = disabledExistingBranches.has(branch.name); const subtitle = branch.worktreePath ? branch.worktreePath === currentWorktreePath ? "Checked out in this thread" : "Checked out in another worktree" : branch.isDefault ? "Default branch" : "Local branch"; return ( <Pressable key={branch.name} className={cn( "gap-1 rounded-[18px] border px-4 py-3 disabled:opacity-[0.45]", branch.current ? "border-subtle-strong" : "border-border", )} disabled={busy || disabled} onPress={() => { void gitActions.onCheckoutSelectedThreadBranch(branch.name).then(() => { navigation.goBack(); }); }} > <View className="absolute inset-0 rounded-[18px] bg-card" /> <Text className="text-foreground text-base font-t3-bold">{branch.name}</Text> <Text className="text-foreground-secondary text-xs font-medium">{subtitle}</Text> </Pressable> ); })} </View> </ScrollView> </View> );}
import { sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
import { useState } from "react";
import { Platform, Pressable, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AndroidSheetHeader } from "../../../components/AndroidScreenHeader";
import { AppText as Text, AppTextInput as TextInput } from "../../../components/AppText";
import { cn } from "../../../lib/cn";
import { useEnvironmentQuery } from "../../../state/query";
import { useThreadSelection } from "../../../state/use-thread-selection";
import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions";
import { useSelectedThreadGitState } from "../../../state/use-selected-thread-git-state";
import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-worktree";
import { vcsEnvironment } from "../../../state/vcs";
import { SheetActionButton } from "./gitSheetComponents";
type GitBranchesSheetProps = StaticScreenProps<{
readonly environmentId: string;
readonly threadId: string;
}>;
export function GitBranchesSheet(_props: GitBranchesSheetProps) {
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const { selectedThread } = useThreadSelection();
const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
const gitState = useSelectedThreadGitState();
const gitActions = useSelectedThreadGitActions();
const gitStatus = useEnvironmentQuery(
selectedThread !== null && selectedThreadCwd !== null
? vcsEnvironment.status({
environmentId: selectedThread.environmentId,
input: { cwd: selectedThreadCwd },
})
: null,
);
const currentBranchLabel = gitStatus.data?.refName ?? selectedThread?.branch ?? "Detached HEAD";
const currentWorktreePath = selectedThreadWorktreePath;
const availableBranches = gitState.selectedThreadBranches;
const branchesLoading = gitState.selectedThreadBranchesLoading;
const busy = gitState.gitOperationLabel !== null;
const [newBranchName, setNewBranchName] = useState("");
const [worktreeBaseBranch, setWorktreeBaseBranch] = useState(
currentBranchLabel === "Detached HEAD" ? "main" : currentBranchLabel,
);
const [worktreeBranchName, setWorktreeBranchName] = useState("");
const disabledExistingBranchNames: Array<string> = [];
for (const branch of availableBranches) {
if (branch.worktreePath !== null && branch.worktreePath !== currentWorktreePath) {
disabledExistingBranchNames.push(branch.name);
}
}
const disabledExistingBranches = new Set(disabledExistingBranchNames);
return (
<View collapsable={false} className="flex-1 bg-sheet">
{Platform.OS === "android" ? (
<AndroidSheetHeader title="Branches & worktrees" onBack={() => navigation.goBack()} />
) : null}
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentInset={{ bottom: Math.max(insets.bottom, 18) + 18 }}
contentContainerClassName="gap-4 px-5 pt-2"
>
<View className="gap-2 rounded-[18px] border border-border bg-card px-4 py-4">
<Text className="text-foreground-secondary text-2xs font-t3-bold tracking-[1px] uppercase">
New branch
</Text>
<TextInput
value={newBranchName}
onChangeText={setNewBranchName}
placeholder="feature/mobile-polish"
className="rounded-[18px]"
/>
<SheetActionButton
icon="plus"
label="Create & checkout"
tone="primary"
disabled={busy || newBranchName.trim().length === 0}
onPress={() => {
const branch = sanitizeFeatureBranchName(newBranchName.trim());
if (branch.length === 0) return;
void gitActions.onCreateSelectedThreadBranch(branch).then(() => {
setNewBranchName("");
navigation.goBack();
});
}}
/>
</View>
<View className="gap-2 rounded-[18px] border border-border bg-card px-4 py-4">
<Text className="text-foreground-secondary text-2xs font-t3-bold tracking-[1px] uppercase">
New worktree
</Text>
<TextInput
value={worktreeBaseBranch}
onChangeText={setWorktreeBaseBranch}
placeholder="main"
className="rounded-[18px]"
/>
<TextInput
value={worktreeBranchName}
onChangeText={setWorktreeBranchName}
placeholder="feature/mobile-thread"
className="rounded-[18px]"
/>
<SheetActionButton
icon="square.split.2x1"
label="Create worktree"
tone="primary"
disabled={
busy ||
worktreeBaseBranch.trim().length === 0 ||
worktreeBranchName.trim().length === 0
}
onPress={() => {
const baseBranch = worktreeBaseBranch.trim();
const newBranch = worktreeBranchName.trim();
if (baseBranch.length === 0 || newBranch.length === 0) return;
void gitActions.onCreateSelectedThreadWorktree({ baseBranch, newBranch }).then(() => {
setWorktreeBranchName("");
navigation.goBack();
});
}}
/>
</View>
<View className="gap-2">
<Text className="text-foreground-secondary text-2xs font-t3-bold tracking-[1px] uppercase">
Existing branches
</Text>
{branchesLoading ? (
<Text className="text-foreground-secondary text-sm font-medium">
Loading branches...
</Text>
) : null}
{!branchesLoading && availableBranches.length === 0 ? (
<Text className="text-foreground-secondary text-sm font-medium">
No local branches found.
</Text>
) : null}
{availableBranches.map((branch) => {
const disabled = disabledExistingBranches.has(branch.name);
const subtitle = branch.worktreePath
? branch.worktreePath === currentWorktreePath
? "Checked out in this thread"
: "Checked out in another worktree"
: branch.isDefault
? "Default branch"
: "Local branch";
return (
<Pressable
key={branch.name}
className={cn(
"gap-1 rounded-[18px] border px-4 py-3 disabled:opacity-[0.45]",
branch.current ? "border-subtle-strong" : "border-border",
)}
disabled={busy || disabled}
onPress={() => {
void gitActions.onCheckoutSelectedThreadBranch(branch.name).then(() => {
navigation.goBack();
});
}}
>
<View className="absolute inset-0 rounded-[18px] bg-card" />
<Text className="text-foreground text-base font-t3-bold">{branch.name}</Text>
<Text className="text-foreground-secondary text-xs font-medium">{subtitle}</Text>
</Pressable>
);
})}
</View>
</ScrollView>
</View>
);
}