import type { VcsRef } from "@t3tools/client-runtime/state/vcs";
import { resolveEnvironmentMachineKind } from "@t3tools/contracts";
import { LegendList } from "@legendapp/list/react-native";
import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import * as Haptics from "expo-haptics";
import { useNavigation } from "@react-navigation/native";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import {
ActivityIndicator,
Alert,
Platform,
Pressable,
ScrollView,
TextInput,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
import { SymbolView } from "../../components/AppSymbol";
import { AppText as Text } from "../../components/AppText";
import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
import { ThemedSwitch } from "../../components/ThemedSwitch";
import { cn } from "../../lib/cn";
import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { useServerConfigs } from "../../state/entities";
import { useAtomCommand } from "../../state/use-atom-command";
import { vcsEnvironment } from "../../state/vcs";
import {
createNativeMailSearchToolbarItem,
NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET,
NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
} from "../layout/native-mail-search-toolbar";
import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider";
import { checkoutNewTaskBranch } from "./checkout-new-task-branch";
function SelectionRow(props: {
readonly icon?: "arrow.triangle.branch" | ReactNode;
readonly onPress: () => void;
readonly disabled?: boolean;
readonly selected: boolean;
readonly isLast?: boolean;
readonly subtitle?: string;
readonly title: string;
}) {
return (
{props.icon === "arrow.triangle.branch" ? (
) : (
(props.icon ?? null)
)}
{props.title}
{props.subtitle ? (
{props.subtitle}
) : null}
{props.selected ? (
) : null}
);
}
function ToggleRow(props: {
readonly title: string;
readonly value: boolean;
readonly onValueChange: (value: boolean) => void;
}) {
return (
{props.title}
);
}
function BranchSelectionRow(props: {
readonly badge: string | null;
readonly branch: VcsRef;
readonly disabled: boolean;
readonly isFirst: boolean;
readonly isLast: boolean;
readonly onSelect: (branch: VcsRef) => void;
readonly selected: boolean;
}) {
const onPress = useCallback(() => props.onSelect(props.branch), [props.branch, props.onSelect]);
return (
);
}
function PickerSurface(props: { readonly children: ReactNode }) {
return {props.children};
}
export function NewTaskEnvironmentPickerRouteScreen() {
const flow = useNewTaskFlow();
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const serverConfigs = useServerConfigs();
return (
{Platform.OS === "android" ? (
navigation.goBack()} />
) : null}
{flow.environments.map((environment, index) => (
}
isLast={index === flow.environments.length - 1}
onPress={() => {
void Haptics.selectionAsync();
flow.selectEnvironment(environment.environmentId);
navigation.goBack();
}}
selected={flow.selectedEnvironmentId === environment.environmentId}
title={environment.environmentLabel}
/>
))}
);
}
export function NewTaskBranchPickerRouteScreen() {
const flow = useNewTaskFlow();
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false });
const [switchingBranchName, setSwitchingBranchName] = useState(null);
const selectingBranchNameRef = useRef(null);
const allowSelectionNavigationRef = useRef(false);
const mountedRef = useRef(true);
const screenTitle = flow.workspaceMode === "worktree" ? "Base branch" : "Branch";
const usesNativeMailSearchToolbar = Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;
const selectedBranchName =
flow.selectedBranchName ??
flow.availableBranches.find((branch) => branch.current)?.name ??
flow.availableBranches.find((branch) => branch.isDefault)?.name ??
null;
const branchListContentStyle = useMemo(
() => ({
paddingBottom: usesNativeMailSearchToolbar
? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET + 16
: Platform.OS === "ios"
? 16
: Math.max(insets.bottom, 16) + 16,
paddingHorizontal: 16,
paddingTop: 12,
}),
[insets.bottom, usesNativeMailSearchToolbar],
);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
flow.setBranchQuery("");
};
}, [flow.setBranchQuery]);
useEffect(
() =>
navigation.addListener("beforeRemove", (event) => {
if (selectingBranchNameRef.current !== null && !allowSelectionNavigationRef.current) {
event.preventDefault();
}
}),
[navigation],
);
const selectBranch = useCallback(
async (branch: VcsRef) => {
if (selectingBranchNameRef.current !== null) {
return;
}
selectingBranchNameRef.current = branch.name;
void Haptics.selectionAsync();
try {
if (!flow.selectedProject) return;
setSwitchingBranchName(branch.name);
const result = await checkoutNewTaskBranch({
branch,
project: flow.selectedProject,
workspaceMode: flow.workspaceMode,
switchRef,
});
if (result._tag === "Failure") {
if (mountedRef.current && navigation.isFocused() && !isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
Alert.alert(
"Could not switch branch",
error instanceof Error ? error.message : "The branch could not be checked out.",
);
}
return;
}
// The checkout has already changed the repository. Persist the matching
// draft selection even if the native sheet was dismissed while the
// command was in flight; only visible-screen work is focus-gated below.
flow.selectBranch(result.value);
if (!mountedRef.current || !navigation.isFocused()) {
return;
}
flow.setBranchQuery("");
allowSelectionNavigationRef.current = true;
navigation.goBack();
} finally {
selectingBranchNameRef.current = null;
allowSelectionNavigationRef.current = false;
if (mountedRef.current) {
setSwitchingBranchName(null);
}
}
},
[
flow.selectBranch,
flow.selectedProject,
flow.setBranchQuery,
flow.workspaceMode,
navigation,
switchRef,
],
);
const renderBranch = useCallback(
({ item, index }: { readonly item: VcsRef; readonly index: number }) => (
),
[
flow.filteredBranches.length,
flow.selectedProject,
selectBranch,
selectedBranchName,
switchingBranchName,
],
);
const branchListHeader =
flow.workspaceMode === "worktree" ? (
) : null;
const branchContent =
flow.filteredBranches.length === 0 ? (
{branchListHeader}
{flow.branchesLoading ? : null}
{flow.branchesLoading
? "Loading branches…"
: flow.branchesError
? flow.branchesError
: flow.branchQuery
? "No matching branches"
: "No branches available"}
{!flow.branchesLoading && flow.branchesError ? (
Try again
) : null}
) : (
`${branch.remoteName ?? "local"}:${branch.name}:${branch.worktreePath ?? ""}`
}
ListHeaderComponent={branchListHeader}
ListFooterComponent={
flow.branchesFetchingNextPage ? (
) : null
}
onEndReached={flow.hasMoreBranches ? flow.loadMoreBranches : undefined}
onEndReachedThreshold={0.35}
renderItem={renderBranch}
showsVerticalScrollIndicator={false}
/>
);
if (Platform.OS === "android") {
return (
navigation.goBack()} />
{branchContent}
);
}
return (
<>
[
createNativeMailSearchToolbarItem({
onSearchTextChange: flow.setBranchQuery,
placeholder: "Find a branch",
searchTextChangeId: "new-task-branch-search-text",
showsSearchDismissButton: true,
}),
]
: undefined,
headerSearchBarOptions: usesNativeMailSearchToolbar
? undefined
: {
allowToolbarIntegration: true,
autoCapitalize: "none",
hideNavigationBar: false,
obscureBackground: false,
placeholder: "Find a branch",
onChangeText: (event) => {
flow.setBranchQuery(event.nativeEvent.text);
},
onCancelButtonPress: () => {
flow.setBranchQuery("");
},
},
}}
/>
{usesNativeMailSearchToolbar ? null : (
)}
{branchContent}
>
);
}