Skip to content
Source
Sign inSign up

use-thread-selection.ts

215 lines8.2 KB

1import { useAtomValue } from "@effect/atom-react";
2import { useRoute, type RouteProp } from "@react-navigation/native";
3import { useState } from "react";
4import {
5 EnvironmentId,
6 ThreadId,
7 type OrchestrationV2ThreadProjection,
8 type OrchestrationV2ThreadShell,
9 type ScopedProjectRef,
10 type ScopedThreadRef,
11} from "@t3tools/contracts";
12import {
13 presentThreadShell,
14 type EnvironmentThreadShell,
15} from "@t3tools/client-runtime/state/shell";
16import {
17 deriveLatestThreadRun,
18 deriveThreadRuntime,
19} from "@t3tools/client-runtime/state/thread-execution";
20import * as Option from "effect/Option";
21​
22import { scopedThreadKey } from "../lib/scopedEntities";
23import { useProject, useQuietThreadShell } from "./entities";
24import { useEnvironmentThread } from "./threads";
25import {
26 resolvePendingThreadCreation,
27 pendingThreadCreationOutcomesAtom,
28 pendingThreadCreationShell,
29 selectThreadCreationDetail,
30 type PendingThreadCreation,
31} from "./pending-thread-creation";
32import {
33 useRemoteEnvironmentRuntime,
34 useSavedRemoteConnection,
35} from "./use-remote-environment-registry";
36import { useThreadOutboxMessages } from "./use-thread-outbox";
37type ThreadSelectionRouteParams = {
38 readonly environmentId?: string | string[];
39 readonly threadId?: string | string[];
40};
41​
42function firstRouteParam(value: string | string[] | undefined): string | null {
43 const first = Array.isArray(value) ? value[0] : value;
44 return first === undefined || first.trim().length === 0 ? null : first;
46​
47function latestUserMessageAt(
48 projection: OrchestrationV2ThreadProjection,
49): OrchestrationV2ThreadShell["latestUserMessageAt"] {
50 for (let index = projection.messages.length - 1; index >= 0; index -= 1) {
51 const message = projection.messages[index];
52 if (message?.role === "user") {
53 return message.createdAt;
54 }
55 }
56​
57 return null;
59​
60function threadDetailToShell(
61 environmentId: EnvironmentId,
62 projection: OrchestrationV2ThreadProjection,
63): EnvironmentThreadShell {
64 const thread = projection.thread;
65 const latestRun = deriveLatestThreadRun(projection);
66 const runtime = deriveThreadRuntime(projection);
67 const pendingRequest =
68 projection.runtimeRequests.find((request) => request.status === "pending") ?? null;
69 return presentThreadShell(environmentId, {
70 id: thread.id,
71 projectId: thread.projectId,
72 title: thread.title,
73 providerInstanceId: thread.providerInstanceId,
74 modelSelection: thread.modelSelection,
75 runtimeMode: thread.runtimeMode,
76 interactionMode: thread.interactionMode,
77 branch: thread.branch,
78 worktreePath: thread.worktreePath,
79 linkedPullRequest: thread.linkedPullRequest ?? null,
80 pullRequests: thread.pullRequests,
81 branchPullRequest: thread.branchPullRequest ?? null,
82 activeProviderThreadId: thread.activeProviderThreadId,
83 lineage: thread.lineage,
84 forkedFrom: thread.forkedFrom,
85 createdBy: thread.createdBy,
86 creationSource: thread.creationSource,
87 latestRunId: latestRun?.runId ?? null,
88 activeRunId: runtime?.activeRunId ?? null,
89 status: runtime?.status ?? "idle",
90 pendingRuntimeRequest:
91 pendingRequest === null
92 ? null
93 : { id: pendingRequest.id, kind: pendingRequest.kind, createdAt: pendingRequest.createdAt },
94 latestVisibleMessage: null,
95 latestUserMessageAt: latestUserMessageAt(projection),
96 hasActionableProposedPlan: false,
97 itemCount: projection.turnItems.length,
98 visibleItemCount: projection.visibleTurnItems.length,
99 createdAt: thread.createdAt,
100 updatedAt: thread.updatedAt,
101 archivedAt: thread.archivedAt,
102 settledOverride: thread.settledOverride,
103 settledAt: thread.settledAt,
104 unsettledAt: thread.unsettledAt,
105 activeOrderKey: thread.activeOrderKey,
106 autoSettleDisabledAt: thread.autoSettleDisabledAt,
107 pinnedAt: thread.pinnedAt,
108 pinOrderKey: thread.pinOrderKey,
109 snoozedUntil: thread.snoozedUntil ?? null,
110 snoozedAt: thread.snoozedAt ?? null,
111 deletedAt: thread.deletedAt,
112 });
114​
115function useResolvedThreadSelection(params: ThreadSelectionRouteParams | undefined) {
116 const routeParams = params ?? {};
117 const routeEnvironmentId = firstRouteParam(routeParams.environmentId);
118 const routeThreadId = firstRouteParam(routeParams.threadId);
119 // Retain the last complete thread ref while closing routes drop params.
120 const [lastRouteThreadRef, setLastRouteThreadRef] = useState<ScopedThreadRef | null>(null);
121 if (
122 routeEnvironmentId !== null &&
123 routeThreadId !== null &&
124 (lastRouteThreadRef?.environmentId !== routeEnvironmentId ||
125 lastRouteThreadRef.threadId !== routeThreadId)
126 ) {
127 setLastRouteThreadRef({
128 environmentId: EnvironmentId.make(routeEnvironmentId),
129 threadId: ThreadId.make(routeThreadId),
130 });
131 }
132 const selectedThreadRef =
133 routeEnvironmentId !== null && routeThreadId !== null
134 ? lastRouteThreadRef?.environmentId === routeEnvironmentId &&
135 lastRouteThreadRef.threadId === routeThreadId
136 ? lastRouteThreadRef
137 : {
138 environmentId: EnvironmentId.make(routeEnvironmentId),
139 threadId: ThreadId.make(routeThreadId),
140 }
141 : lastRouteThreadRef;
142 const selectedThreadShell = useQuietThreadShell(selectedThreadRef);
143 const selectedThreadKey =
144 selectedThreadRef === null
145 ? null
146 : scopedThreadKey(selectedThreadRef.environmentId, selectedThreadRef.threadId);
147 const queuedMessagesByThreadKey = useThreadOutboxMessages();
148 const creationOutcome = useAtomValue(pendingThreadCreationOutcomesAtom);
149 const { pendingCreation, detailRef: selectedThreadDetailRef } = selectThreadCreationDetail({
150 threadRef: selectedThreadRef,
151 threadKey: selectedThreadKey,
152 queuedMessagesByThreadKey,
153 outcomes: creationOutcome,
154 });
155 const [previousCreation, setPreviousCreation] = useState<PendingThreadCreation | null>(null);
156 const needsDetail =
157 selectedThreadShell === null || pendingCreation !== null || previousCreation !== null;
158 const selectedThreadDetailState = useEnvironmentThread(
159 needsDetail ? (selectedThreadDetailRef?.environmentId ?? null) : null,
160 needsDetail ? (selectedThreadDetailRef?.threadId ?? null) : null,
161 );
162 const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data);
163 const selectedThread =
164 selectedThreadShell ??
165 (selectedThreadRef !== null && selectedThreadDetail !== null
166 ? threadDetailToShell(selectedThreadRef.environmentId, selectedThreadDetail)
167 : pendingCreation !== null
168 ? pendingThreadCreationShell(pendingCreation.message)
169 : null);
170 const resolvedCreation = resolvePendingThreadCreation({
171 threadKey: selectedThreadKey,
172 pending: pendingCreation,
173 previous: previousCreation,
174 detail: selectedThreadDetail,
175 });
176 // Compare pendingCreation contents; new render-time objects would otherwise continually set state.
177 const creationUnchanged =
178 resolvedCreation === previousCreation ||
179 (resolvedCreation !== null &&
180 previousCreation !== null &&
181 resolvedCreation.message === previousCreation.message &&
182 resolvedCreation.outcome === previousCreation.outcome);
183 if (!creationUnchanged) {
184 setPreviousCreation(resolvedCreation);
185 }
186 const selectedThreadCreation = creationUnchanged ? previousCreation : resolvedCreation;
187 const selectedProjectRef: ScopedProjectRef | null =
188 selectedThread === null
189 ? null
190 : {
191 environmentId: selectedThread.environmentId,
192 projectId: selectedThread.projectId,
193 };
194 const selectedThreadProject = useProject(selectedProjectRef);
195 const selectedEnvironmentId = selectedThread?.environmentId ?? null;
196 const selectedEnvironmentConnection = useSavedRemoteConnection(selectedEnvironmentId);
197 const selectedEnvironmentRuntime = useRemoteEnvironmentRuntime(selectedEnvironmentId);
198​
199 return {
200 selectedThreadRef,
201 selectedThread,
202 selectedThreadCreation,
203 selectedThreadDetailRef,
204 selectedThreadProject,
205 selectedEnvironmentConnection,
206 selectedEnvironmentRuntime,
207 };
209​
210type ThreadSelectionState = ReturnType<typeof useResolvedThreadSelection>;
211​
212export function useThreadSelection(): ThreadSelectionState {
213 const route = useRoute<RouteProp<Record<string, ThreadSelectionRouteParams | undefined>>>();
214 return useResolvedThreadSelection(route.params);