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(() => { 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(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(() => { 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(null); const selectedThreadCreation = resolvePendingThreadCreation({ threadKey: selectedThreadKey, pending: pendingCreation, previous: previousCreation, detail: selectedThreadDetail, }); if (previousCreation !== selectedThreadCreation) { setPreviousCreation(selectedThreadCreation); } const selectedProjectRef = useMemo( () => 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; export function useThreadSelection(): ThreadSelectionState { const route = useRoute>>(); return useResolvedThreadSelection(route.params); }