203 lines · 7.2 KB · TypeScript
RawDownload
import { useAtomValue } from "@effect/atom-react";import { useRoute, type RouteProp } from "@react-navigation/native";import { useMemo, useRef, useState } from "react";import {  EnvironmentId,  type OrchestrationThread,  ThreadId,  type ScopedProjectRef,  type ScopedThreadRef,} from "@t3tools/contracts";import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";import * as Option from "effect/Option"; import { scopedThreadKey } from "../lib/scopedEntities";import { useProject, useThreadShell } from "../state/entities";import { useEnvironmentThread } from "../state/threads";import {  resolvePendingThreadCreation,  pendingThreadCreationOutcomesAtom,  pendingThreadCreationShell,  type PendingThreadCreation,} from "./pending-thread-creation";import {  useRemoteEnvironmentRuntime,  useSavedRemoteConnection,} from "./use-remote-environment-registry";import { useThreadOutboxMessages } from "./use-thread-outbox";type ThreadSelectionRouteParams = {  readonly environmentId?: string | string[];  readonly threadId?: string | string[];}; function firstRouteParam(value: string | string[] | undefined): string | null {  if (Array.isArray(value)) {    return value[0] ?? null;  }   return value ?? null;} function latestUserMessageAt(thread: OrchestrationThread): OrchestrationThread["updatedAt"] | null {  for (let index = thread.messages.length - 1; index >= 0; index -= 1) {    const message = thread.messages[index];    if (message?.role === "user") {      return message.createdAt;    }  }   return null;} function threadDetailToShell(  environmentId: EnvironmentId,  thread: OrchestrationThread,): EnvironmentThreadShell {  return {    environmentId,    id: thread.id,    projectId: thread.projectId,    title: thread.title,    modelSelection: thread.modelSelection,    runtimeMode: thread.runtimeMode,    interactionMode: thread.interactionMode,    branch: thread.branch,    worktreePath: thread.worktreePath,    linkedPullRequest: thread.linkedPullRequest ?? null,    pullRequests: thread.pullRequests,    branchPullRequest: thread.branchPullRequest ?? null,    latestTurn: thread.latestTurn,    createdAt: thread.createdAt,    updatedAt: thread.updatedAt,    archivedAt: thread.archivedAt,    settledOverride: thread.settledOverride,    settledAt: thread.settledAt,    unsettledAt: thread.unsettledAt,    activeOrderKey: thread.activeOrderKey,    pinnedAt: thread.pinnedAt,    pinOrderKey: thread.pinOrderKey,    snoozedUntil: thread.snoozedUntil ?? null,    snoozedAt: thread.snoozedAt ?? null,    session: thread.session,    latestUserMessageAt: latestUserMessageAt(thread),    hasPendingApprovals: false,    hasPendingUserInput: false,    hasActionableProposedPlan: false,  };} function useResolvedThreadSelection(params: ThreadSelectionRouteParams | undefined) {  const routeParams = params ?? {};  const routeThreadRef = useMemo<ScopedThreadRef | null>(() => {    const environmentId = firstRouteParam(routeParams.environmentId);    const threadId = firstRouteParam(routeParams.threadId);    if (!environmentId || !threadId) {      return null;    }     return {      environmentId: EnvironmentId.make(environmentId),      threadId: ThreadId.make(threadId),    };  }, [routeParams.environmentId, routeParams.threadId]);  const lastRouteThreadRef = useRef<ScopedThreadRef | null>(null);  if (routeThreadRef !== null) {    lastRouteThreadRef.current = routeThreadRef;  }  const selectedThreadRef = routeThreadRef ?? lastRouteThreadRef.current;  const selectedThreadShell = useThreadShell(selectedThreadRef);  const selectedThreadKey =    selectedThreadRef === null      ? null      : scopedThreadKey(selectedThreadRef.environmentId, selectedThreadRef.threadId);  const queuedMessagesByThreadKey = useThreadOutboxMessages();  const creationOutcome = useAtomValue(pendingThreadCreationOutcomesAtom);  // A creation the outbox still holds or just delivered: the thread screen  // opened before the server made the thread, so present a stand-in shell.  const pendingCreation = useMemo<PendingThreadCreation | null>(() => {    if (selectedThreadKey === null) {      return null;    }    const queued = queuedMessagesByThreadKey[selectedThreadKey]?.find(      (message) => message.creation !== undefined,    );    const outcome = creationOutcome[selectedThreadKey] ?? null;    const message = queued ?? outcome?.message ?? null;    return message === null ? null : { message, outcome };  }, [creationOutcome, queuedMessagesByThreadKey, selectedThreadKey]);  // Until the creation is delivered the server has no thread to subscribe  // to; subscribing anyway would retry "not found" for the whole setup.  const selectedThreadDetailRef =    selectedThreadShell !== null ||    pendingCreation === null ||    pendingCreation.outcome?.kind === "delivered"      ? selectedThreadRef      : null;  const selectedThreadDetailState = useEnvironmentThread(    selectedThreadDetailRef?.environmentId ?? null,    selectedThreadDetailRef?.threadId ?? null,  );  const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data);  const selectedThread = useMemo(    () =>      selectedThreadShell ??      (selectedThreadRef !== null && selectedThreadDetail !== null        ? threadDetailToShell(selectedThreadRef.environmentId, selectedThreadDetail)        : pendingCreation !== null          ? pendingThreadCreationShell(pendingCreation.message)          : null),    [pendingCreation, selectedThreadDetail, selectedThreadRef, selectedThreadShell],  );  const [previousCreation, setPreviousCreation] = useState<PendingThreadCreation | null>(null);  const selectedThreadCreation = resolvePendingThreadCreation({    threadKey: selectedThreadKey,    pending: pendingCreation,    previous: previousCreation,    detail: selectedThreadDetail,  });  if (previousCreation !== selectedThreadCreation) {    setPreviousCreation(selectedThreadCreation);  }  const selectedProjectRef = useMemo<ScopedProjectRef | null>(    () =>      selectedThread === null        ? null        : {            environmentId: selectedThread.environmentId,            projectId: selectedThread.projectId,          },    [selectedThread],  );  const selectedThreadProject = useProject(selectedProjectRef);  const selectedEnvironmentId = selectedThread?.environmentId ?? null;  const selectedEnvironmentConnection = useSavedRemoteConnection(selectedEnvironmentId);  const selectedEnvironmentRuntime = useRemoteEnvironmentRuntime(selectedEnvironmentId);   return useMemo(    () => ({      selectedThreadRef,      selectedThread,      selectedThreadCreation,      selectedThreadDetailState,      selectedThreadProject,      selectedEnvironmentConnection,      selectedEnvironmentRuntime,    }),    [      selectedEnvironmentConnection,      selectedEnvironmentRuntime,      selectedThread,      selectedThreadCreation,      selectedThreadDetailState,      selectedThreadProject,      selectedThreadRef,    ],  );} type ThreadSelectionState = ReturnType<typeof useResolvedThreadSelection>; export function useThreadSelection(): ThreadSelectionState {  const route = useRoute<RouteProp<Record<string, ThreadSelectionRouteParams | undefined>>>();  return useResolvedThreadSelection(route.params);}