import { useRoute, type RouteProp } from "@react-navigation/native"; import { useMemo, useRef } from "react"; import { EnvironmentId, ThreadId, type OrchestrationV2ThreadProjection, type OrchestrationV2ThreadShell, type ScopedProjectRef, type ScopedThreadRef, } from "@t3tools/contracts"; import { presentThreadShell, type EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; import { deriveLatestThreadRun, deriveThreadRuntime, } from "@t3tools/client-runtime/state/thread-execution"; import * as Option from "effect/Option"; import { useProject, useThreadShell } from "../state/entities"; import { useEnvironmentThread } from "../state/threads"; import { useRemoteEnvironmentRuntime, useSavedRemoteConnection, } from "./use-remote-environment-registry"; 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( projection: OrchestrationV2ThreadProjection, ): OrchestrationV2ThreadShell["latestUserMessageAt"] { for (let index = projection.messages.length - 1; index >= 0; index -= 1) { const message = projection.messages[index]; if (message?.role === "user") { return message.createdAt; } } return null; } /** * Builds an optimistic thread shell from the detail projection for the window * where the shell list has not materialized the thread yet (e.g. a thread that * was just created from this device). */ function threadDetailToShell( environmentId: EnvironmentId, projection: OrchestrationV2ThreadProjection, ): EnvironmentThreadShell { const thread = projection.thread; const latestRun = deriveLatestThreadRun(projection); const runtime = deriveThreadRuntime(projection); const pendingRequest = projection.runtimeRequests.find((request) => request.status === "pending") ?? null; return presentThreadShell(environmentId, { id: thread.id, projectId: thread.projectId, title: thread.title, providerInstanceId: thread.providerInstanceId, modelSelection: thread.modelSelection, runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, branch: thread.branch, worktreePath: thread.worktreePath, activeProviderThreadId: thread.activeProviderThreadId, lineage: thread.lineage, forkedFrom: thread.forkedFrom, createdBy: thread.createdBy, creationSource: thread.creationSource, latestRunId: latestRun?.runId ?? null, activeRunId: runtime?.activeRunId ?? null, status: runtime?.status ?? "idle", pendingRuntimeRequest: pendingRequest === null ? null : { id: pendingRequest.id, kind: pendingRequest.kind, createdAt: pendingRequest.createdAt }, latestVisibleMessage: null, latestUserMessageAt: latestUserMessageAt(projection), hasActionableProposedPlan: false, itemCount: projection.turnItems.length, visibleItemCount: projection.visibleTurnItems.length, createdAt: thread.createdAt, updatedAt: thread.updatedAt, archivedAt: thread.archivedAt, settledOverride: thread.settledOverride, settledAt: thread.settledAt, snoozedUntil: thread.snoozedUntil ?? null, snoozedAt: thread.snoozedAt ?? null, deletedAt: thread.deletedAt, }); } 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 selectedThreadDetailState = useEnvironmentThread( selectedThreadRef?.environmentId ?? null, selectedThreadRef?.threadId ?? null, ); const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data); const selectedThread = useMemo( () => selectedThreadShell ?? (selectedThreadRef !== null && selectedThreadDetail !== null ? threadDetailToShell(selectedThreadRef.environmentId, selectedThreadDetail) : null), [selectedThreadDetail, selectedThreadRef, selectedThreadShell], ); 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, selectedThreadProject, selectedEnvironmentConnection, selectedEnvironmentRuntime, }), [ selectedEnvironmentConnection, selectedEnvironmentRuntime, selectedThread, selectedThreadProject, selectedThreadRef, ], ); } type ThreadSelectionState = ReturnType; export function useThreadSelection(): ThreadSelectionState { const route = useRoute>>(); return useResolvedThreadSelection(route.params); }