1158 lines · 41.9 KB · TypeScript
RawDownload
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.