705 lines · 24.7 KB · TypeScript
RawDownload
import { NativeStackScreenOptions } from "../../native/StackHeader";import { StackActions, useNavigation } from "@react-navigation/native";import { useCallback, useEffect, useMemo, useRef } from "react";import { Alert, InteractionManager, 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 { 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 { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip";import { ControlPillMenu } from "../../components/ControlPill";import { ProviderIcon } from "../../components/ProviderIcon"; 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 { getComposerDraftSnapshot } from "../../state/use-composer-drafts";import { useProjects } from "../../state/entities";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"; 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;}) {  const projects = useProjects();  const createProjectThread = useCreateProjectThread();  const flow = useNewTaskFlow();  const navigation = useNavigation();  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 appliedInitialProjectKeyRef = useRef<string | null>(null);  useEffect(() => {    return () => {      appliedInitialProjectKeyRef.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 borderColor = useThemeColor("--color-border");  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 (selectedProject) {      return;    }     if (logicalProjects.length === 1) {      setProject(logicalProjects[0]!.project);      return;    }     navigation.dispatch(StackActions.replace("NewTask"));  }, [    logicalProjects,    projects,    props.initialProjectRef,    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(() => {    if (!selectedProject) {      return;    }     let focusFrame: ReturnType<typeof requestAnimationFrame> | null = null;    const interaction = InteractionManager.runAfterInteractions(() => {      focusFrame = requestAnimationFrame(() => promptInputRef.current?.focus());    });     return () => {      interaction.cancel();      if (focusFrame !== null) {        cancelAnimationFrame(focusFrame);      }    };  }, [selectedProject]);   const environmentMenuActions = useMemo(    () =>      flow.environments.map((environment) => ({        id: `environment:${environment.environmentId}`,        title: environment.environmentLabel,        state:          flow.selectedEnvironmentId === environment.environmentId ? ("on" as const) : undefined,      })),    [flow.environments, flow.selectedEnvironmentId],  );   const modelMenuActions = useMemo(    () =>      flow.providerGroups.map((group) => ({        id: `provider:${group.providerKey}`,        title: group.providerLabel,        subtitle: group.models.find(          (model) =>            flow.selectedModel &&            model.selection.instanceId === flow.selectedModel.instanceId &&            model.selection.model === flow.selectedModel.model,        )?.label,        subactions: group.models.map((option) => ({          id: `model:${option.key}`,          title: option.label,          state:            flow.selectedModel &&            option.selection.instanceId === flow.selectedModel.instanceId &&            option.selection.model === flow.selectedModel.model              ? ("on" as const)              : undefined,        })),      })),    [flow.providerGroups, flow.selectedModel],  );  const providerOptionDescriptors = useMemo(    () =>      resolveProviderOptionDescriptors({        capabilities: flow.selectedModelOption?.capabilities,        selections: flow.selectedModel?.options,      }),    [flow.selectedModel?.options, flow.selectedModelOption?.capabilities],  );   const optionsMenuActions = useMemo(    () => [      ...buildProviderOptionMenuActions(providerOptionDescriptors),      {        id: "options-runtime",        title: "Runtime",        subtitle:          flow.runtimeMode === "approval-required"            ? "Approve actions"            : flow.runtimeMode === "auto-accept-edits"              ? "Auto-accept edits"              : "Full access",        subactions: [          { id: "options:runtime:approval-required", title: "Approve actions" },          { id: "options:runtime:auto-accept-edits", title: "Auto-accept edits" },          { id: "options:runtime:full-access", title: "Full access" },        ].map((option) => {          const value = option.id.replace("options:runtime:", "");          return {            id: option.id,            title: option.title,            state: flow.runtimeMode === value ? ("on" as const) : undefined,          };        }),      },      {        id: "options-interaction",        title: "Interaction",        subtitle: flow.interactionMode === "plan" ? "Plan" : "Default",        subactions: [          { id: "options:interaction:default", title: "Default" },          { id: "options:interaction:plan", title: "Plan" },        ].map((option) => {          const value = option.id.replace("options:interaction:", "");          return {            id: option.id,            title: option.title,            state: flow.interactionMode === value ? ("on" as const) : undefined,          };        }),      },    ],    [flow.interactionMode, flow.runtimeMode, providerOptionDescriptors],  );   const workspaceMenuActions = useMemo(() => {    const branchActions =      flow.availableBranches.length === 0        ? [            {              id: "workspace:branch:none",              title: flow.branchesLoading ? "Loading branches…" : "No branches available",              attributes: { disabled: true },            },          ]        : flow.availableBranches.slice(0, 12).map((branch) => {            const badge = branchBadgeLabel({              branch,              project: flow.selectedProject,            });             return {              id: `workspace:branch:${branch.name}`,              title: branch.name,              subtitle: badge ? badge.toUpperCase() : undefined,              state: flow.selectedBranchName === branch.name ? ("on" as const) : undefined,            };          });     return [      {        id: "workspace:mode",        title: "Mode",        subtitle: flow.workspaceMode === "local" ? "Current checkout" : "New worktree",        subactions: (["local", "worktree"] as const).map((value) => ({          id: `workspace:mode:${value}`,          title: value === "local" ? "Current checkout" : "New worktree",          state: flow.workspaceMode === value ? ("on" as const) : undefined,        })),      },      {        id: "workspace:branch",        title: "Branch",        subtitle: flow.selectedBranchName ?? "Choose branch",        subactions: branchActions,      },      ...(flow.workspaceMode === "worktree"        ? [            {              id: "workspace:start-from-origin",              title: "Start from origin",              subtitle: "Base the worktree on the latest origin branch",              image: "arrow.triangle.pull",              state: flow.startFromOrigin ? ("on" as const) : undefined,            },          ]        : []),    ];  }, [    flow.availableBranches,    flow.branchesLoading,    flow.selectedBranchName,    flow.selectedProject,    flow.startFromOrigin,    flow.workspaceMode,  ]);   const selectedEnvironmentLabel =    flow.environments.find(      (environment) => environment.environmentId === flow.selectedEnvironmentId,    )?.environmentLabel ?? "Environment";  const currentBranchName =    flow.availableBranches.find((branch) => branch.current)?.name ??    flow.availableBranches.find((branch) => branch.isDefault)?.name ??    null;  const configurationLabel = useMemo(    () => providerOptionsConfigurationLabel(providerOptionDescriptors),    [providerOptionDescriptors],  );  const workspaceLabel = useMemo(    () =>      formatWorkspaceLabel({        currentBranchName,        selectedBranchName: flow.selectedBranchName,        workspaceMode: flow.workspaceMode,      }),    [currentBranchName, flow.selectedBranchName, flow.workspaceMode],  );  function handleModelMenuAction(event: string) {    if (!event.startsWith("model:")) {      return;    }    flow.setSelectedModelKey(event.slice("model:".length));  }   function handleEnvironmentMenuAction(event: string) {    if (!event.startsWith("environment:")) {      return;    }    flow.selectEnvironment(EnvironmentId.make(event.slice("environment:".length)));  }   function handleOptionsMenuAction(event: string) {    const providerOptions = applyProviderOptionMenuEvent(providerOptionDescriptors, event);    if (providerOptions) {      flow.setSelectedModelOptions(providerOptions);      return;    }    if (event.startsWith("options:runtime:")) {      flow.setRuntimeMode(        event.slice("options:runtime:".length) as Parameters<typeof flow.setRuntimeMode>[0],      );      return;    }    if (event.startsWith("options:interaction:")) {      flow.setInteractionMode(        event.slice("options:interaction:".length) as Parameters<typeof flow.setInteractionMode>[0],      );    }  }   function handleWorkspaceMenuAction(event: string) {    if (event.startsWith("workspace:mode:")) {      flow.setWorkspaceMode(        event.slice("workspace:mode:".length) as Parameters<typeof flow.setWorkspaceMode>[0],      );      return;    }    if (event === "workspace:start-from-origin") {      flow.setStartFromOrigin(!flow.startFromOrigin);      return;    }    if (event.startsWith("workspace:branch:")) {      const branchName = event.slice("workspace:branch:".length);      const branch = flow.availableBranches.find((candidate) => candidate.name === branchName);      if (branch) {        flow.selectBranch(branch);      }    }  }   async function handlePickImages(): Promise<void> {    const result = await pickComposerImages({ existingCount: flow.attachments.length });    if (result.images.length > 0) {      flow.appendAttachments(result.images);    }  }   const handleNativePasteImages = useCallback(    async (uris: ReadonlyArray<string>) => {      try {        const images = await convertPastedImagesToAttachments({          uris,          existingCount: flow.attachments.length,        });        if (images.length > 0) {          flow.appendAttachments(images);        }      } catch (error) {        console.error("[native paste] error converting images", error);      }    },    [flow],  );   async function handleStart(): Promise<void> {    const selectedProject = flow.selectedProject;    const draftKey = flow.draftKey;    if (!selectedProject || !draftKey) {      return;    }    const draft = getComposerDraftSnapshot(draftKey);    const modelSelection = draft.modelSelection ?? flow.selectedModel;    const workspaceMode = draft.workspaceSelection?.mode ?? flow.workspaceMode;    const selectedBranchName = draft.workspaceSelection?.branch ?? flow.selectedBranchName;    const selectedWorktreePath =      draft.workspaceSelection?.worktreePath ?? flow.selectedWorktreePath;    const startFromOrigin = draft.workspaceSelection?.startFromOrigin ?? flow.startFromOrigin;    const runtimeMode = draft.runtimeMode ?? flow.runtimeMode;    const interactionMode = draft.interactionMode ?? flow.interactionMode;    const initialMessageText = draft.text.trim();     if (      !modelSelection ||      initialMessageText.length === 0 ||      flow.submitting ||      (workspaceMode === "worktree" && !selectedBranchName)    ) {      return;    }     const editingPendingTask = flow.editingPendingTask;     if (!environmentConnected) {      // Offline: park the task in the outbox; the drain sends it when the      // environment reconnects. Editing an existing pending task re-queues it      // under its original identifiers.      const metadata = editingPendingTask        ? {            threadId: editingPendingTask.threadId,            commandId: editingPendingTask.commandId,            messageId: editingPendingTask.messageId,            createdAt: editingPendingTask.createdAt,          }        : makeTurnCommandMetadata();      const message = flow.buildPendingTaskMessage(metadata);

Lines 1–500 of 705.