1import { useNavigation, usePreventRemove, type StaticScreenProps } from "@react-navigation/native";
2import { useEffect, useMemo, useRef, useState } from "react";
3import { Alert, View } from "react-native";
4import {
5 isAtomCommandInterrupted,
6 squashAtomCommandFailure,
7} from "@t3tools/client-runtime/state/runtime";
8import { AppText as Text } from "../../components/AppText";
9import { useProjects } from "../../state/entities";
10import { useAtomCommand } from "../../state/use-atom-command";
11import { useWorkspaceState } from "../../state/workspace";
12import { vcsEnvironment } from "../../state/vcs";
13import { checkoutNewTaskBranch } from "./checkout-new-task-branch";
14import { NativeStackScreenOptions } from "../../native/StackHeader";
15
16import { NewTaskDraftScreen } from "./NewTaskDraftScreen";
17
18type NewTaskDraftRouteParams = {
19 readonly environmentId?: string | string[];
20 readonly projectId?: string | string[];
21 readonly branch?: string | null;
22 readonly worktreePath?: string | null;
23 readonly title?: string | string[];
24 /** Set by Add Project when this draft opens while the project's clone runs. */
25 readonly cloning?: string | string[];
26 readonly pendingTaskId?: string | string[];
27 readonly draftId?: string | string[];
28 readonly incomingShareId?: string | string[];
29};
30
31export function NewTaskDraftRouteScreen({ route }: StaticScreenProps<NewTaskDraftRouteParams>) {
32 const params = useMemo(() => route.params ?? {}, [route.params]);
33 const pendingTaskId = Array.isArray(params.pendingTaskId)
34 ? params.pendingTaskId[0]
35 : params.pendingTaskId;
36 const draftId = Array.isArray(params.draftId) ? params.draftId[0] : params.draftId;
37 const projects = useProjects();
38 const { state: catalogState } = useWorkspaceState();
39 const navigation = useNavigation();
40 const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false });
41
42 // Keyed on the params object so a fresh navigation to this (already
43 // mounted) screen produces a new reference, letting the draft screen
44 // re-apply the requested project.
45 const initialProjectRef = useMemo(
46 () => ({
47 environmentId: Array.isArray(params.environmentId)
48 ? params.environmentId[0]
49 : params.environmentId,
50 projectId: Array.isArray(params.projectId) ? params.projectId[0] : params.projectId,
51 branch: params.branch,
52 worktreePath: params.worktreePath,
53 cloning: (Array.isArray(params.cloning) ? params.cloning[0] : params.cloning) === "1",
54 }),
55 [params],
56 );
57
58 const [preparation, setPreparation] = useState<{
59 request: typeof initialProjectRef;
60 result: Awaited<ReturnType<typeof checkoutNewTaskBranch>>;
61 workspaceRoot: string | undefined;
62 } | null>(null);
63 const project = projects.find(
64 (candidate) =>
65 candidate.environmentId === initialProjectRef.environmentId &&
66 candidate.id === initialProjectRef.projectId,
67 );
68 const environmentId = project?.environmentId;
69 const workspaceRoot = project?.workspaceRoot;
70 const needsPreparation = Boolean(initialProjectRef.branch && !pendingTaskId && !draftId);
71
72 const [pendingCheckouts, setPendingCheckouts] = useState(0);
73 const checkoutTail = useRef(Promise.resolve());
74 const waitingForProject =
75 !project &&
76 (catalogState.isLoadingConnections ||
77 (!catalogState.hasLoadedShellSnapshot &&
78 catalogState.hasConnectingEnvironment &&
79 catalogState.connectionError === null));
80
81 useEffect(() => {
82 if (!needsPreparation || !initialProjectRef.branch || waitingForProject) return;
83 const branchName = initialProjectRef.branch;
84 let active = true;
85 setPendingCheckouts((count) => count + 1);
86 // Serialize replacements: ignoring a stale result cannot undo its Git mutation.
87 checkoutTail.current = checkoutTail.current.then(async () => {
88 if (!active) {
89 setPendingCheckouts((count) => count - 1);
90 return;
91 }
92 const result = await checkoutNewTaskBranch({
93 // A thread's branch is historical; only switchRef can establish that
94 // the shared project checkout now matches it.
95 branch: {
96 name: branchName,
97 current: false,
98 isDefault: false,
99 worktreePath: initialProjectRef.worktreePath ?? null,
100 },
101 project: environmentId && workspaceRoot ? { environmentId, workspaceRoot } : null,
102 workspaceMode: "local",
103 switchRef,
104 });
105 setPendingCheckouts((count) => count - 1);
106 if (active) setPreparation({ request: initialProjectRef, result, workspaceRoot });
107 });
108 return () => {
109 active = false;
110 };
111 }, [
112 environmentId,
113 workspaceRoot,
114 initialProjectRef,
115 needsPreparation,
116 switchRef,
117 waitingForProject,
118 ]);
119
120 const result =
121 preparation?.request === initialProjectRef && preparation.workspaceRoot === workspaceRoot
122 ? preparation.result
123 : null;
124 // The native-stack guard covers iOS swipe dismissal as well as back actions.
125 // A replaced request must settle too before the shared checkout is left behind.
126 const checkoutPending = pendingCheckouts > 0 || (needsPreparation && result === null);
127 usePreventRemove(checkoutPending, () => undefined);
128 useEffect(() => {
129 if (checkoutPending || result?._tag !== "Failure") return;
130 if (!isAtomCommandInterrupted(result)) {
131 const error = squashAtomCommandFailure(result);
132 Alert.alert(
133 "Could not switch branch",
134 error instanceof Error ? error.message : "The branch could not be checked out.",
135 );
136 }
137 navigation.goBack();
138 }, [checkoutPending, result, navigation]);
139
140 const preparedProjectRef = useMemo(
141 () =>
142 result?._tag === "Success"
143 ? { ...initialProjectRef, branch: result.value.name }
144 : initialProjectRef,
145 [initialProjectRef, result],
146 );
147 // Send/queue remain unavailable on failure while the unlocked route closes.
148 const preparingBranch = checkoutPending || (needsPreparation && result?._tag !== "Success");
149
150 return (
151 <>
152 <NativeStackScreenOptions
153 options={{
154 title: Array.isArray(params.title) ? params.title[0] : (params.title ?? "New task"),
155 }}
156 />
157 {preparingBranch ? (
158 <View className="flex-1 items-center justify-center bg-screen">
159 <Text className="text-foreground">Switching branch...</Text>
160 </View>
161 ) : (
162 <NewTaskDraftScreen
163 initialProjectRef={preparedProjectRef}
164 incomingShareId={
165 Array.isArray(params.incomingShareId)
166 ? params.incomingShareId[0]
167 : params.incomingShareId
168 }
169 pendingTaskId={pendingTaskId}
170 draftId={draftId}
171 />
172 )}
173 </>
174 );
175}