Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {
linkToRootPath,
rootNavigationRef,
} from "./lib/rootNavigation";
import { pruneNewTaskFlowStates } from "./features/threads/new-task-flow-state";
import { pruneNewTaskBranchCheckouts } from "./features/threads/NewTaskDraftRouteScreen";

import "../global.css";
Expand Down Expand Up @@ -133,6 +134,7 @@ function handleNavigationStateChange(state: NavigationState | undefined): void {
if (!state) return;
setAgentNotificationPathname(workspaceLocationFromState(state).pathname);
recordAppShortcutNavigationState(state);
pruneNewTaskFlowStates(state);
pruneNewTaskBranchCheckouts(state);
pruneIncomingShareImports(state);
}
Expand Down
14 changes: 12 additions & 2 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
NewTaskThreadSettingsRouteScreen,
} from "./features/threads/ThreadSettingsSheet";
import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider";
import { newTaskFlowKey } from "./features/threads/new-task-flow-state";
import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen";
import { SettingsNavigationRouteScreen } from "./features/settings/SettingsNavigationRouteScreen";
import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppearanceRouteScreen";
Expand Down Expand Up @@ -500,7 +501,12 @@ function RootStackLayout(props: {
.slice(0, props.state.index + 1)
.filter((route) => !isWorkspaceOverlayRoute(route.name)),
);
const draftRoute = props.state.routes.findLast((route) => route.name === "Thread");
// The flow follows the draft route even while a pushed Thread covers it, so
// its state lives exactly as long as the route.
const draftRoute = props.state.routes.findLast(isDraftThreadRoute);
const draftRouteOnTop =
draftRoute !== undefined &&
props.state.routes.findLast((route) => route.name === "Thread") === draftRoute;
const draftParams = (
draftRoute?.params as { draft?: { draftId?: string | string[] } } | undefined
)?.draft;
Expand All @@ -509,7 +515,11 @@ function RootStackLayout(props: {
: draftParams?.draftId;

return (
<NewTaskFlowProvider active={isDraftThreadRoute(draftRoute)} routeDraftId={routeDraftId}>
<NewTaskFlowProvider
active={draftRouteOnTop}
flowKey={draftRoute === undefined ? undefined : newTaskFlowKey(draftRoute)}
routeDraftId={routeDraftId}
>
<HardwareKeyboardCommandProvider pathname={pathname}>
<ShowcaseCaptureCoordinator pathname={pathname} />
<SettingsEnvironmentFilterProvider open={settingsOpen}>
Expand Down
11 changes: 1 addition & 10 deletions apps/mobile/src/features/threads/NewTaskDraftRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useNewTaskFlow } from "./new-task-flow-provider";
import * as Dialog from "../../components/ui/Dialog";
import {
CommonActions,
Expand All @@ -7,7 +6,7 @@ import {
usePreventRemoveContext,
type StaticScreenProps,
} from "@react-navigation/native";
import { useEffect, useLayoutEffect, useState } from "react";
import { useEffect, useState } from "react";
import { View } from "react-native";
import { useAtomValue } from "@effect/atom-react";
import {
Expand Down Expand Up @@ -91,13 +90,6 @@ const idleBranchCheckoutAtom = Atom.make(AsyncResult.initial<BranchCheckoutResul
export function NewTaskDraftRouteScreen({ route }: StaticScreenProps<NewTaskDraftRouteParams>) {
const currentRoute = useRoute();
const params = route.params ?? {};
const { reset } = useNewTaskFlow();
const [initialized, setInitialized] = useState(false);
useLayoutEffect(() => {
reset();
// oxlint-disable-next-line react/set-state-in-effect -- Draft reset runs once before first paint; initialized gates the screen until then.
setInitialized(true);
}, [reset]);
const draftId = Array.isArray(params.draftId) ? params.draftId[0] : params.draftId;
const projects = useProjects();
const { state: catalogState } = useWorkspaceState();
Expand Down Expand Up @@ -201,7 +193,6 @@ export function NewTaskDraftRouteScreen({ route }: StaticScreenProps<NewTaskDraf
};
const preparingBranch = checkoutPending || (needsPreparation && result?._tag !== "Success");

if (!initialized) return null;
return (
<>
{preparingBranch ? (
Expand Down
52 changes: 10 additions & 42 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ import {
getComposerDraftSnapshot,
updateComposerDraftSettings,
scheduleUnusedComposerAttachmentCleanup,
waitForComposerDraftsLoaded,
} from "../../state/use-composer-drafts";
import { useEnvironmentServerConfig, useProjects } from "../../state/entities";
import { useProjectClone } from "../../state/projectClones";
Expand Down Expand Up @@ -223,10 +222,6 @@ export function NewTaskDraftScreen(props: {
}
};
const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false });
const selectedProjectRef = useRef(selectedProject);
useEffect(() => {
selectedProjectRef.current = selectedProject;
}, [selectedProject]);
const removeClonedProject = async () => {
if (!selectedProject) return;
const removed = selectedProject;
Expand All @@ -242,8 +237,7 @@ export function NewTaskDraftScreen(props: {
);
return;
}
const current = selectedProjectRef.current;
if (current?.id === removed.id && current.environmentId === removed.environmentId) {
if (flow.getSelectedProjectKey() === scopedProjectKey(removed.environmentId, removed.id)) {
navigation.dispatch(StackActions.replace("Home"));
}
};
Expand Down Expand Up @@ -280,8 +274,12 @@ export function NewTaskDraftScreen(props: {
project.id === props.initialProjectRef?.projectId,
),
);
// A route naming a draft that is gone, or whose project is gone, also
// returns to the picker.
const isRouteDraftMissing =
props.draftId !== undefined && flow.missingRouteDraftId === props.draftId;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const isProjectPickerReturnActive =
isReturningToProjectPicker && !requestedInitialProjectAvailable;
(isReturningToProjectPicker && !requestedInitialProjectAvailable) || isRouteDraftMissing;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const isIncomingShareAwaitingServerConfig = Boolean(
incomingShare?.attachments.some((attachment) => attachment.type === "file") &&
selectedEnvironmentServerConfig === null,
Expand Down Expand Up @@ -373,12 +371,11 @@ export function NewTaskDraftScreen(props: {
isIncomingShareUnavailable;
const appliedInitialProjectKeyRef = useRef<string | null>(null);
useEffect(() => {
if (!isReturningToProjectPicker) {
return;
}
if (requestedInitialProjectAvailable) {
if (isReturningToProjectPicker && requestedInitialProjectAvailable) {
// oxlint-disable-next-line react/set-state-in-effect -- Ends the return-to-picker flow once the requested project arrives from the catalog.
setIsReturningToProjectPicker(false);
}
if (!isProjectPickerReturnActive) {
return;
}
// Wait for usePreventRemove to disable before replacing, or the transfer guard swallows the fallback.
Expand All @@ -389,41 +386,12 @@ export function NewTaskDraftScreen(props: {
});
return () => cancelAnimationFrame(frame);
}, [
isProjectPickerReturnActive,
isReturningToProjectPicker,
navigation,
props.incomingShareId,
requestedInitialProjectAvailable,
]);
const { openDraft } = flow;
// Cold launches load drafts and projects separately; wait for hydration and retry while shells load.
const attemptedDraftIdRef = useRef<string | null>(null);
useEffect(() => {
if (!props.draftId) {
return;
}
const draftId = props.draftId;
if (attemptedDraftIdRef.current === draftId) {
return;
}
let cancelled = false;
void waitForComposerDraftsLoaded().then(() => {
if (cancelled || attemptedDraftIdRef.current === draftId) {
return;
}
if (openDraft(draftId)) {
attemptedDraftIdRef.current = draftId;
return;
}
if (getComposerDraftSnapshot(draftId).project !== undefined && projects.length === 0) {
return;
}
attemptedDraftIdRef.current = draftId;
navigation.dispatch(CommonActions.navigate("NewTaskProject"));
});
return () => {
cancelled = true;
};
}, [navigation, openDraft, projects, props.draftId]);

const lastInitialProjectRefRef = useRef(props.initialProjectRef);
const initialPickerRef = useRef<"open" | "dismissed" | null>(null);
Expand Down
Loading
Loading