import { NativeStackScreenOptions } from "../../native/StackHeader";import { StackActions, useNavigation, usePreventRemove } from "@react-navigation/native";import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { Alert, InteractionManager, Platform, View, useColorScheme } from "react-native";import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller";import { useSafeAreaInsets } from "react-native-safe-area-context";import { useThemeColor } from "../../lib/useThemeColor";import { useFontFamily } from "../../lib/useFontFamily"; import { EnvironmentId } from "@t3tools/contracts";import { isAtomCommandInterrupted, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime"; import { ComposerEditor, type ComposerEditorHandle } from "../../components/ComposerEditor";import { ComposerToolbarButton, ComposerToolbarRow, ComposerToolbarScroller, ComposerToolbarTrigger,} from "../../components/ComposerToolbarTrigger";import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip";import { ControlPill, ControlPillMenu } from "../../components/ControlPill";import { ProviderIcon } from "../../components/ProviderIcon";import { ComposerSurface } from "./ThreadComposer"; import { makeTurnCommandMetadata } from "../../lib/commandMetadata";import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages";import { applyProviderOptionMenuEvent, buildProviderOptionMenuActions, providerOptionsConfigurationLabel, resolveProviderOptionDescriptors,} from "../../lib/providerOptions";import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";import { clearComposerDraftContent, getComposerDraftSnapshot, mergeComposerDraftContent, restoreComposerDraftSnapshot, type ComposerDraft,} from "../../state/use-composer-drafts";import { useProjects } from "../../state/entities";import { deriveThreadTitleFromPrompt } from "../../lib/projectThreadStartTurn";import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration";import { enqueueThreadOutboxMessage, removeThreadOutboxMessage } from "../../state/thread-outbox";import { useRemoteConnectionStatus } from "../../state/use-remote-environment-registry";import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider";import { useCreateProjectThread } from "./use-project-actions";import { useIncomingShare } from "../sharing/IncomingShareProvider"; function formatWorkspaceLabel(input: { readonly workspaceMode: string; readonly currentBranchName: string | null; readonly selectedBranchName: string | null;}): string { const branchName = input.selectedBranchName ?? input.currentBranchName; if (input.workspaceMode === "worktree") { return branchName ? `New worktree · ${branchName}` : "New worktree"; } return branchName ? `Current · ${branchName}` : "Current checkout";} export function NewTaskDraftScreen(props: { readonly initialProjectRef?: { readonly environmentId?: string; readonly projectId?: string; }; /** Queued outbox message id when editing an existing pending task. */ readonly pendingTaskId?: string; /** Durable native share inbox item to merge into this project draft. */ readonly incomingShareId?: string;}) { const projects = useProjects(); const createProjectThread = useCreateProjectThread(); const flow = useNewTaskFlow(); const navigation = useNavigation(); const { consumeShare, getShare, isLoading: isIncomingShareInboxLoading, releaseShareReservation, reserveShare, } = useIncomingShare(); const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const isKeyboardVisible = useKeyboardState((state) => state.isVisible); const controlsBottomPadding = isKeyboardVisible ? 8 : Math.max(insets.bottom, 10); const { logicalProjects, selectedProject, setProject } = flow; const { connectedEnvironments } = useRemoteConnectionStatus(); const environmentConnected = selectedProject !== null && connectedEnvironments.find( (environment) => environment.environmentId === selectedProject.environmentId, )?.connectionState === "connected"; const promptInputRef = useRef<ComposerEditorHandle>(null); const loadedBranchesProjectKeyRef = useRef<string | null>(null); const [isComposerFocused, setIsComposerFocused] = useState(false); const [importingShareKey, setImportingShareKey] = useState<string | null>(null); const [isCancellingShareImport, setIsCancellingShareImport] = useState(false); const [cancelledIncomingShareId, setCancelledIncomingShareId] = useState<string | null>(null); const [isReturningToProjectPicker, setIsReturningToProjectPicker] = useState(false); const [shareImportAttempt, setShareImportAttempt] = useState(0); const startedShareImportKeyRef = useRef<string | null>(null); const cancellingShareImportKeyRef = useRef<string | null>(null); const shareImportDraftBackupRef = useRef(new Map<string, ComposerDraft>()); const activeShareImportTokenRef = useRef<symbol | null>(null); const shareImportMountedRef = useRef(true); const latestDraftKeyRef = useRef(flow.draftKey); const latestIncomingShareIdRef = useRef(props.incomingShareId); latestDraftKeyRef.current = flow.draftKey; latestIncomingShareIdRef.current = props.incomingShareId; const isImportingShare = importingShareKey !== null; const alertedUnavailableIncomingShareIdRef = useRef<string | null>(null); const incomingShare = props.incomingShareId ? getShare(props.incomingShareId) : null; const requestedInitialProjectAvailable = Boolean( props.initialProjectRef?.environmentId && props.initialProjectRef.projectId && projects.some( (project) => project.environmentId === props.initialProjectRef?.environmentId && project.id === props.initialProjectRef?.projectId, ), ); const isProjectPickerReturnActive = isReturningToProjectPicker && !requestedInitialProjectAvailable; const isIncomingShareTransferPending = Boolean( incomingShare && cancelledIncomingShareId !== props.incomingShareId, ); usePreventRemove( (isIncomingShareTransferPending && !isProjectPickerReturnActive) || isCancellingShareImport, () => undefined, ); const hasImportedIncomingShare = Boolean( props.incomingShareId && flow.draftKey && getComposerDraftSnapshot(flow.draftKey).importedShareIds?.includes(props.incomingShareId), ); const isIncomingShareUnavailable = Boolean( props.incomingShareId && !isIncomingShareInboxLoading && !incomingShare && !hasImportedIncomingShare, ); const isIncomingShareReady = !props.incomingShareId || (hasImportedIncomingShare && !incomingShare) || isIncomingShareUnavailable; const appliedInitialProjectKeyRef = useRef<string | null>(null); useEffect(() => { if (cancelledIncomingShareId === props.incomingShareId) { navigation.goBack(); } }, [cancelledIncomingShareId, navigation, props.incomingShareId]); useEffect(() => { if (!isReturningToProjectPicker) { return; } if (requestedInitialProjectAvailable) { setIsReturningToProjectPicker(false); return; } // Let usePreventRemove commit its disabled state before replacing this // route, otherwise the transfer guard can swallow the fallback action. const frame = requestAnimationFrame(() => { navigation.dispatch( StackActions.replace("NewTask", { incomingShareId: props.incomingShareId }), ); }); return () => cancelAnimationFrame(frame); }, [ isReturningToProjectPicker, navigation, props.incomingShareId, requestedInitialProjectAvailable, ]); useEffect(() => { if (!shareImportMountedRef.current) { startedShareImportKeyRef.current = null; } shareImportMountedRef.current = true; return () => { appliedInitialProjectKeyRef.current = null; shareImportMountedRef.current = false; activeShareImportTokenRef.current = null; cancellingShareImportKeyRef.current = null; }; }, []); const { beginEditingPendingTask, cancelEditingPendingTask, editingPendingTask } = flow; const attemptedPendingTaskIdRef = useRef<string | null>(null); useEffect(() => { if (!props.pendingTaskId || editingPendingTask?.messageId === props.pendingTaskId) { return; } // Attempt each pending task once: after it is delivered or deleted the // editing session legitimately ends, and re-running must not navigate. if (attemptedPendingTaskIdRef.current === props.pendingTaskId) { return; } attemptedPendingTaskIdRef.current = props.pendingTaskId; if (!beginEditingPendingTask(props.pendingTaskId)) { // The queued task no longer exists (sent or deleted before opening). navigation.dispatch(StackActions.replace("NewTask")); } }, [beginEditingPendingTask, editingPendingTask?.messageId, navigation, props.pendingTaskId]); useEffect(() => { if (!props.pendingTaskId) return; return () => { // Allow a later navigation for the same pending task to re-hydrate it. attemptedPendingTaskIdRef.current = null; cancelEditingPendingTask(); }; }, [props.pendingTaskId, cancelEditingPendingTask]); const foregroundColor = useThemeColor("--color-foreground"); const regularFontFamily = useFontFamily("regular"); const bodyText = useScaledTextRole("body"); const headlineText = useScaledTextRole("headline"); const sheetFadeOpaque = colorScheme === "dark" ? "rgba(14,14,14,0.98)" : "rgba(242,242,247,0.98)"; const sheetFadeTransparent = colorScheme === "dark" ? "rgba(14,14,14,0)" : "rgba(242,242,247,0)"; // A new navigation to this mounted screen delivers a fresh initialProjectRef // reference — treat it as a new request and let it apply again. const lastInitialProjectRefRef = useRef(props.initialProjectRef); useEffect(() => { // Pending-task editing owns project selection (and must not fall through // to the replace("NewTask") fallback while its hydration is in flight). if (props.pendingTaskId) { return; } if (lastInitialProjectRefRef.current !== props.initialProjectRef) { lastInitialProjectRefRef.current = props.initialProjectRef; appliedInitialProjectKeyRef.current = null; } const initialEnvironmentId = props.initialProjectRef?.environmentId; const initialProjectId = props.initialProjectRef?.projectId; if (initialEnvironmentId && initialProjectId) { const directProject = projects.find( (project) => project.environmentId === initialEnvironmentId && project.id === initialProjectId, ) ?? null; if (directProject) { // Apply the route's project once. Re-applying on every change would // instantly revert environment/project switches made in the picker. const directProjectKey = `${directProject.environmentId}:${directProject.id}`; if (appliedInitialProjectKeyRef.current === directProjectKey) { return; } appliedInitialProjectKeyRef.current = directProjectKey; if ( selectedProject?.environmentId === directProject.environmentId && selectedProject.id === directProject.id ) { return; } setProject(directProject); return; } if (projects.length > 0) { // Never fall through to the flow provider's temporary first-project // default. Return to the picker with the share id intact so the user // can choose an available destination. setIsReturningToProjectPicker(true); } return; } if (selectedProject) { return; } if (logicalProjects.length === 1) { setProject(logicalProjects[0]!.project); return; } navigation.dispatch(StackActions.replace("NewTask")); }, [ logicalProjects, projects, props.initialProjectRef, props.incomingShareId, props.pendingTaskId, navigation, selectedProject, setProject, ]); useEffect(() => { if (!selectedProject) { loadedBranchesProjectKeyRef.current = null; return; } const projectKey = `${selectedProject.environmentId}:${selectedProject.id}`; if (loadedBranchesProjectKeyRef.current === projectKey) { return; } loadedBranchesProjectKeyRef.current = projectKey; void flow.loadBranches(); }, [flow.loadBranches, selectedProject]); useEffect(() => { const shareId = props.incomingShareId; const draftKey = flow.draftKey; const destinationProject = selectedProject; const initialEnvironmentId = props.initialProjectRef?.environmentId; const initialProjectId = props.initialProjectRef?.projectId; const selectedProjectMatchesRoute = !initialEnvironmentId || !initialProjectId || (destinationProject?.environmentId === initialEnvironmentId && destinationProject.id === initialProjectId); if ( !shareId || !draftKey || !destinationProject || !selectedProjectMatchesRoute || cancelledIncomingShareId === shareId ) { return; } const importKey = `${shareId}:${draftKey}`; if ( startedShareImportKeyRef.current === importKey || cancellingShareImportKeyRef.current === importKey ) { return; } if (!incomingShare) { if (isIncomingShareUnavailable && alertedUnavailableIncomingShareIdRef.current !== shareId) { alertedUnavailableIncomingShareIdRef.current = shareId; Alert.alert( "Shared content unavailable", "The shared content is no longer in the inbox. You can continue editing this task draft.", ); } return; } if (alertedUnavailableIncomingShareIdRef.current === shareId) { alertedUnavailableIncomingShareIdRef.current = null; } startedShareImportKeyRef.current = importKey; const draftBackup = shareImportDraftBackupRef.current.get(importKey) ?? getComposerDraftSnapshot(draftKey); shareImportDraftBackupRef.current.set(importKey, draftBackup); const importToken = Symbol(importKey); let didReserveShare = false; let needsDraftRestore = false; activeShareImportTokenRef.current = importToken; setImportingShareKey(importKey); void (async () => { await reserveShare(shareId, { environmentId: String(destinationProject.environmentId), projectId: String(destinationProject.id), }); didReserveShare = true; if ( !shareImportMountedRef.current || activeShareImportTokenRef.current !== importToken || latestDraftKeyRef.current !== draftKey || latestIncomingShareIdRef.current !== shareId ) { return; } needsDraftRestore = true; const { skippedAttachmentCount } = await mergeComposerDraftContent(draftKey, { text: incomingShare.text, attachments: incomingShare.attachments, sourceShareId: shareId, }); if ( !shareImportMountedRef.current || activeShareImportTokenRef.current !== importToken || latestDraftKeyRef.current !== draftKey || latestIncomingShareIdRef.current !== shareId ) { // The durable reservation makes an interrupted transfer resume only // in this project instead of copying into a second project draft. return; } await consumeShare(shareId); if (!shareImportMountedRef.current || activeShareImportTokenRef.current !== importToken) { return; } const warnings = [...incomingShare.warnings]; if (skippedAttachmentCount > 0) { warnings.push( `${skippedAttachmentCount} shared image${skippedAttachmentCount === 1 ? " was" : "s were"} skipped because this draft reached the attachment limit.`, ); } if (warnings.length > 0) { Alert.alert("Some shared content was skipped", warnings.join("\n")); } shareImportDraftBackupRef.current.delete(importKey); })() .catch((error) => { if (!shareImportMountedRef.current || activeShareImportTokenRef.current !== importToken) { return; } Alert.alert( "Could not import shared content", error instanceof Error ? error.message : "The shared content could not be saved.", [ { text: "Cancel import", style: "cancel", onPress: () => { const cancelImport = async (): Promise<void> => { if (!shareImportMountedRef.current) { return; } // Latch synchronously before restoring the draft. The // restore publishes atom state and can re-run the import // effect before React commits the cancelling state update. cancellingShareImportKeyRef.current = importKey; setIsCancellingShareImport(true); try { if (needsDraftRestore) { await restoreComposerDraftSnapshot(draftKey, draftBackup); needsDraftRestore = false; } if (didReserveShare) { await releaseShareReservation(shareId, { environmentId: String(destinationProject.environmentId), projectId: String(destinationProject.id), }); } shareImportDraftBackupRef.current.delete(importKey); if (shareImportMountedRef.current) { setIsCancellingShareImport(false); setCancelledIncomingShareId(shareId); } } catch (cancelError) { if (!shareImportMountedRef.current) { return; } Alert.alert( "Could not cancel import", cancelError instanceof Error ? cancelError.message : "The shared content could not be restored safely.", [ { text: "Retry import", onPress: () => { cancellingShareImportKeyRef.current = null; setIsCancellingShareImport(false); setShareImportAttempt((attempt) => attempt + 1); }, }, { text: "Retry cancel", onPress: () => void cancelImport(), }, ], { cancelable: false }, ); } }; void cancelImport(); }, }, { text: "Retry", onPress: () => setShareImportAttempt((attempt) => attempt + 1), }, ], { cancelable: false }, ); }) .finally(() => { if (startedShareImportKeyRef.current === importKey) { // Every terminal path, including an invalidated operation, must // release the synchronous start latch so this transfer can retry. startedShareImportKeyRef.current = null; } if (shareImportMountedRef.current && activeShareImportTokenRef.current === importToken) { activeShareImportTokenRef.current = null; setImportingShareKey(null); } }); }, [ consumeShare, cancelledIncomingShareId, flow.draftKey, hasImportedIncomingShare, incomingShare, isIncomingShareInboxLoading, isIncomingShareUnavailable, props.incomingShareId,Lines 1–500 of 1158. Load more
import { NativeStackScreenOptions } from "../../native/StackHeader"; import { StackActions, useNavigation, usePreventRemove } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alert, InteractionManager, Platform, View, useColorScheme } from "react-native"; import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; import { useFontFamily } from "../../lib/useFontFamily"; import { EnvironmentId } from "@t3tools/contracts"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { ComposerEditor, type ComposerEditorHandle } from "../../components/ComposerEditor"; import { ComposerToolbarButton, ComposerToolbarRow, ComposerToolbarScroller, ComposerToolbarTrigger, } from "../../components/ComposerToolbarTrigger"; import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip"; import { ControlPill, ControlPillMenu } from "../../components/ControlPill"; import { ProviderIcon } from "../../components/ProviderIcon"; import { ComposerSurface } from "./ThreadComposer"; import { makeTurnCommandMetadata } from "../../lib/commandMetadata"; import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages"; import { applyProviderOptionMenuEvent, buildProviderOptionMenuActions, providerOptionsConfigurationLabel, resolveProviderOptionDescriptors, } from "../../lib/providerOptions"; import { useScaledTextRole } from "../settings/appearance/useScaledTextRole"; import { clearComposerDraftContent, getComposerDraftSnapshot, mergeComposerDraftContent, restoreComposerDraftSnapshot, type ComposerDraft, } from "../../state/use-composer-drafts"; import { useProjects } from "../../state/entities"; import { deriveThreadTitleFromPrompt } from "../../lib/projectThreadStartTurn"; import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration"; import { enqueueThreadOutboxMessage, removeThreadOutboxMessage } from "../../state/thread-outbox"; import { useRemoteConnectionStatus } from "../../state/use-remote-environment-registry"; import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider"; import { useCreateProjectThread } from "./use-project-actions"; import { useIncomingShare } from "../sharing/IncomingShareProvider"; function formatWorkspaceLabel(input: { readonly workspaceMode: string; readonly currentBranchName: string | null; readonly selectedBranchName: string | null; }): string { const branchName = input.selectedBranchName ?? input.currentBranchName; if (input.workspaceMode === "worktree") { return branchName ? `New worktree · ${branchName}` : "New worktree"; } return branchName ? `Current · ${branchName}` : "Current checkout"; } export function NewTaskDraftScreen(props: { readonly initialProjectRef?: { readonly environmentId?: string; readonly projectId?: string; }; /** Queued outbox message id when editing an existing pending task. */ readonly pendingTaskId?: string; /** Durable native share inbox item to merge into this project draft. */ readonly incomingShareId?: string; }) { const projects = useProjects(); const createProjectThread = useCreateProjectThread(); const flow = useNewTaskFlow(); const navigation = useNavigation(); const { consumeShare, getShare, isLoading: isIncomingShareInboxLoading, releaseShareReservation, reserveShare, } = useIncomingShare(); const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const isKeyboardVisible = useKeyboardState((state) => state.isVisible); const controlsBottomPadding = isKeyboardVisible ? 8 : Math.max(insets.bottom, 10); const { logicalProjects, selectedProject, setProject } = flow; const { connectedEnvironments } = useRemoteConnectionStatus(); const environmentConnected = selectedProject !== null && connectedEnvironments.find( (environment) => environment.environmentId === selectedProject.environmentId, )?.connectionState === "connected"; const promptInputRef = useRef<ComposerEditorHandle>(null); const loadedBranchesProjectKeyRef = useRef<string | null>(null); const [isComposerFocused, setIsComposerFocused] = useState(false); const [importingShareKey, setImportingShareKey] = useState<string | null>(null); const [isCancellingShareImport, setIsCancellingShareImport] = useState(false); const [cancelledIncomingShareId, setCancelledIncomingShareId] = useState<string | null>(null); const [isReturningToProjectPicker, setIsReturningToProjectPicker] = useState(false); const [shareImportAttempt, setShareImportAttempt] = useState(0); const startedShareImportKeyRef = useRef<string | null>(null); const cancellingShareImportKeyRef = useRef<string | null>(null); const shareImportDraftBackupRef = useRef(new Map<string, ComposerDraft>()); const activeShareImportTokenRef = useRef<symbol | null>(null); const shareImportMountedRef = useRef(true); const latestDraftKeyRef = useRef(flow.draftKey); const latestIncomingShareIdRef = useRef(props.incomingShareId); latestDraftKeyRef.current = flow.draftKey; latestIncomingShareIdRef.current = props.incomingShareId; const isImportingShare = importingShareKey !== null; const alertedUnavailableIncomingShareIdRef = useRef<string | null>(null); const incomingShare = props.incomingShareId ? getShare(props.incomingShareId) : null; const requestedInitialProjectAvailable = Boolean( props.initialProjectRef?.environmentId && props.initialProjectRef.projectId && projects.some( (project) => project.environmentId === props.initialProjectRef?.environmentId && project.id === props.initialProjectRef?.projectId, ), ); const isProjectPickerReturnActive = isReturningToProjectPicker && !requestedInitialProjectAvailable; const isIncomingShareTransferPending = Boolean( incomingShare && cancelledIncomingShareId !== props.incomingShareId, ); usePreventRemove( (isIncomingShareTransferPending && !isProjectPickerReturnActive) || isCancellingShareImport, () => undefined, ); const hasImportedIncomingShare = Boolean( props.incomingShareId && flow.draftKey && getComposerDraftSnapshot(flow.draftKey).importedShareIds?.includes(props.incomingShareId), ); const isIncomingShareUnavailable = Boolean( props.incomingShareId && !isIncomingShareInboxLoading && !incomingShare && !hasImportedIncomingShare, ); const isIncomingShareReady = !props.incomingShareId || (hasImportedIncomingShare && !incomingShare) || isIncomingShareUnavailable; const appliedInitialProjectKeyRef = useRef<string | null>(null); useEffect(() => { if (cancelledIncomingShareId === props.incomingShareId) { navigation.goBack(); } }, [cancelledIncomingShareId, navigation, props.incomingShareId]); useEffect(() => { if (!isReturningToProjectPicker) { return; } if (requestedInitialProjectAvailable) { setIsReturningToProjectPicker(false); return; } // Let usePreventRemove commit its disabled state before replacing this // route, otherwise the transfer guard can swallow the fallback action. const frame = requestAnimationFrame(() => { navigation.dispatch( StackActions.replace("NewTask", { incomingShareId: props.incomingShareId }), ); }); return () => cancelAnimationFrame(frame); }, [ isReturningToProjectPicker, navigation, props.incomingShareId, requestedInitialProjectAvailable, ]); useEffect(() => { if (!shareImportMountedRef.current) { startedShareImportKeyRef.current = null; } shareImportMountedRef.current = true; return () => { appliedInitialProjectKeyRef.current = null; shareImportMountedRef.current = false; activeShareImportTokenRef.current = null; cancellingShareImportKeyRef.current = null; }; }, []); const { beginEditingPendingTask, cancelEditingPendingTask, editingPendingTask } = flow; const attemptedPendingTaskIdRef = useRef<string | null>(null); useEffect(() => { if (!props.pendingTaskId || editingPendingTask?.messageId === props.pendingTaskId) { return; } // Attempt each pending task once: after it is delivered or deleted the // editing session legitimately ends, and re-running must not navigate. if (attemptedPendingTaskIdRef.current === props.pendingTaskId) { return; } attemptedPendingTaskIdRef.current = props.pendingTaskId; if (!beginEditingPendingTask(props.pendingTaskId)) { // The queued task no longer exists (sent or deleted before opening). navigation.dispatch(StackActions.replace("NewTask")); } }, [beginEditingPendingTask, editingPendingTask?.messageId, navigation, props.pendingTaskId]); useEffect(() => { if (!props.pendingTaskId) return; return () => { // Allow a later navigation for the same pending task to re-hydrate it. attemptedPendingTaskIdRef.current = null; cancelEditingPendingTask(); }; }, [props.pendingTaskId, cancelEditingPendingTask]); const foregroundColor = useThemeColor("--color-foreground"); const regularFontFamily = useFontFamily("regular"); const bodyText = useScaledTextRole("body"); const headlineText = useScaledTextRole("headline"); const sheetFadeOpaque = colorScheme === "dark" ? "rgba(14,14,14,0.98)" : "rgba(242,242,247,0.98)"; const sheetFadeTransparent = colorScheme === "dark" ? "rgba(14,14,14,0)" : "rgba(242,242,247,0)"; // A new navigation to this mounted screen delivers a fresh initialProjectRef // reference — treat it as a new request and let it apply again. const lastInitialProjectRefRef = useRef(props.initialProjectRef); useEffect(() => { // Pending-task editing owns project selection (and must not fall through // to the replace("NewTask") fallback while its hydration is in flight). if (props.pendingTaskId) { return; } if (lastInitialProjectRefRef.current !== props.initialProjectRef) { lastInitialProjectRefRef.current = props.initialProjectRef; appliedInitialProjectKeyRef.current = null; } const initialEnvironmentId = props.initialProjectRef?.environmentId; const initialProjectId = props.initialProjectRef?.projectId; if (initialEnvironmentId && initialProjectId) { const directProject = projects.find( (project) => project.environmentId === initialEnvironmentId && project.id === initialProjectId, ) ?? null; if (directProject) { // Apply the route's project once. Re-applying on every change would // instantly revert environment/project switches made in the picker. const directProjectKey = `${directProject.environmentId}:${directProject.id}`; if (appliedInitialProjectKeyRef.current === directProjectKey) { return; } appliedInitialProjectKeyRef.current = directProjectKey; if ( selectedProject?.environmentId === directProject.environmentId && selectedProject.id === directProject.id ) { return; } setProject(directProject); return; } if (projects.length > 0) { // Never fall through to the flow provider's temporary first-project // default. Return to the picker with the share id intact so the user // can choose an available destination. setIsReturningToProjectPicker(true); } return; } if (selectedProject) { return; } if (logicalProjects.length === 1) { setProject(logicalProjects[0]!.project); return; } navigation.dispatch(StackActions.replace("NewTask")); }, [ logicalProjects, projects, props.initialProjectRef, props.incomingShareId, props.pendingTaskId, navigation, selectedProject, setProject, ]); useEffect(() => { if (!selectedProject) { loadedBranchesProjectKeyRef.current = null; return; } const projectKey = `${selectedProject.environmentId}:${selectedProject.id}`; if (loadedBranchesProjectKeyRef.current === projectKey) { return; } loadedBranchesProjectKeyRef.current = projectKey; void flow.loadBranches(); }, [flow.loadBranches, selectedProject]); useEffect(() => { const shareId = props.incomingShareId; const draftKey = flow.draftKey; const destinationProject = selectedProject; const initialEnvironmentId = props.initialProjectRef?.environmentId; const initialProjectId = props.initialProjectRef?.projectId; const selectedProjectMatchesRoute = !initialEnvironmentId || !initialProjectId || (destinationProject?.environmentId === initialEnvironmentId && destinationProject.id === initialProjectId); if ( !shareId || !draftKey || !destinationProject || !selectedProjectMatchesRoute || cancelledIncomingShareId === shareId ) { return; } const importKey = `${shareId}:${draftKey}`; if ( startedShareImportKeyRef.current === importKey || cancellingShareImportKeyRef.current === importKey ) { return; } if (!incomingShare) { if (isIncomingShareUnavailable && alertedUnavailableIncomingShareIdRef.current !== shareId) { alertedUnavailableIncomingShareIdRef.current = shareId; Alert.alert( "Shared content unavailable", "The shared content is no longer in the inbox. You can continue editing this task draft.", ); } return; } if (alertedUnavailableIncomingShareIdRef.current === shareId) { alertedUnavailableIncomingShareIdRef.current = null; } startedShareImportKeyRef.current = importKey; const draftBackup = shareImportDraftBackupRef.current.get(importKey) ?? getComposerDraftSnapshot(draftKey); shareImportDraftBackupRef.current.set(importKey, draftBackup); const importToken = Symbol(importKey); let didReserveShare = false; let needsDraftRestore = false; activeShareImportTokenRef.current = importToken; setImportingShareKey(importKey); void (async () => { await reserveShare(shareId, { environmentId: String(destinationProject.environmentId), projectId: String(destinationProject.id), }); didReserveShare = true; if ( !shareImportMountedRef.current || activeShareImportTokenRef.current !== importToken || latestDraftKeyRef.current !== draftKey || latestIncomingShareIdRef.current !== shareId ) { return; } needsDraftRestore = true; const { skippedAttachmentCount } = await mergeComposerDraftContent(draftKey, { text: incomingShare.text, attachments: incomingShare.attachments, sourceShareId: shareId, }); if ( !shareImportMountedRef.current || activeShareImportTokenRef.current !== importToken || latestDraftKeyRef.current !== draftKey || latestIncomingShareIdRef.current !== shareId ) { // The durable reservation makes an interrupted transfer resume only // in this project instead of copying into a second project draft. return; } await consumeShare(shareId); if (!shareImportMountedRef.current || activeShareImportTokenRef.current !== importToken) { return; } const warnings = [...incomingShare.warnings]; if (skippedAttachmentCount > 0) { warnings.push( `${skippedAttachmentCount} shared image${skippedAttachmentCount === 1 ? " was" : "s were"} skipped because this draft reached the attachment limit.`, ); } if (warnings.length > 0) { Alert.alert("Some shared content was skipped", warnings.join("\n")); } shareImportDraftBackupRef.current.delete(importKey); })() .catch((error) => { if (!shareImportMountedRef.current || activeShareImportTokenRef.current !== importToken) { return; } Alert.alert( "Could not import shared content", error instanceof Error ? error.message : "The shared content could not be saved.", [ { text: "Cancel import", style: "cancel", onPress: () => { const cancelImport = async (): Promise<void> => { if (!shareImportMountedRef.current) { return; } // Latch synchronously before restoring the draft. The // restore publishes atom state and can re-run the import // effect before React commits the cancelling state update. cancellingShareImportKeyRef.current = importKey; setIsCancellingShareImport(true); try { if (needsDraftRestore) { await restoreComposerDraftSnapshot(draftKey, draftBackup); needsDraftRestore = false; } if (didReserveShare) { await releaseShareReservation(shareId, { environmentId: String(destinationProject.environmentId), projectId: String(destinationProject.id), }); } shareImportDraftBackupRef.current.delete(importKey); if (shareImportMountedRef.current) { setIsCancellingShareImport(false); setCancelledIncomingShareId(shareId); } } catch (cancelError) { if (!shareImportMountedRef.current) { return; } Alert.alert( "Could not cancel import", cancelError instanceof Error ? cancelError.message : "The shared content could not be restored safely.", [ { text: "Retry import", onPress: () => { cancellingShareImportKeyRef.current = null; setIsCancellingShareImport(false); setShareImportAttempt((attempt) => attempt + 1); }, }, { text: "Retry cancel", onPress: () => void cancelImport(), }, ], { cancelable: false }, ); } }; void cancelImport(); }, }, { text: "Retry", onPress: () => setShareImportAttempt((attempt) => attempt + 1), }, ], { cancelable: false }, ); }) .finally(() => { if (startedShareImportKeyRef.current === importKey) { // Every terminal path, including an invalidated operation, must // release the synchronous start latch so this transfer can retry. startedShareImportKeyRef.current = null; } if (shareImportMountedRef.current && activeShareImportTokenRef.current === importToken) { activeShareImportTokenRef.current = null; setImportingShareKey(null); } }); }, [ consumeShare, cancelledIncomingShareId, flow.draftKey, hasImportedIncomingShare, incomingShare, isIncomingShareInboxLoading, isIncomingShareUnavailable, props.incomingShareId,