Skip to content
Source
Sign inSign up
Open History to see the last commit

useComposerSelection.ts

260 lines10.1 KB

1import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
2import type { UnifiedSettings } from "@t3tools/contracts";
3import type { Dispatch } from "react";
4import type { ComposerThreadTarget, DraftSessionState } from "../../composerDraftStore";
5import { useStableCallback } from "../../hooks/useStableCallback";
6import { useApplyThreadModelPick } from "./useApplyThreadModelPick";
7import {
8 type EnvironmentId,
9 type ModelSelection,
10 ProviderInstanceId,
11 type ServerProvider,
12 type ThreadId,
13 RuntimeMode,
14} from "@t3tools/contracts";
15import type { deriveThreadStructureRuntime } from "@t3tools/client-runtime/state/thread-execution";
16import { scopeThreadRef } from "@t3tools/client-runtime/environment";
17import { type SetStateAction } from "react";
18import { useNavigate } from "@tanstack/react-router";
19import { toastManager } from "../ui/toast";
20import { resolveAppModelSelectionForInstance } from "../../modelSelection";
21import { resolveNewDraftStartFromOrigin } from "../../lib/chatThreadActions";
22import { type DraftThreadEnvMode, useComposerDraftStore } from "../../composerDraftStore";
23import {
24 getStartedThreadModelChangeBlockReason,
25 deriveLockedProvider,
26 makeModelDisabledReason,
27} from "../ChatView.logic";
28interface ComposerSelectionInput {
29 readonly environmentId: EnvironmentId;
30 readonly composerDraftTarget: ComposerThreadTarget;
31 readonly draftThread: DraftSessionState | null;
32 readonly activeThread: EnvironmentThreadShell | undefined;
33 readonly activeRuntime: ReturnType<typeof deriveThreadStructureRuntime>;
34 readonly activeProjectSettings: {
35 readonly settings: { readonly newWorktreesStartFromOrigin: boolean };
36 };
37 readonly settings: UnifiedSettings;
38 readonly providerStatuses: ReadonlyArray<ServerProvider>;
39 readonly lockedProvider: ReturnType<typeof deriveLockedProvider>;
40 readonly supportsProviderSwitchingViaHandoff: boolean;
41 readonly multipleModelSelections: ReadonlyArray<ModelSelection> | null;
42 readonly runtimeMode: RuntimeMode;
43 readonly isLocalDraftThread: boolean;
44 readonly canOverrideServerThreadEnvMode: boolean;
45 readonly setPendingServerThreadEnvModeEntry: Dispatch<
46 SetStateAction<{ readonly threadId: ThreadId; readonly mode: DraftThreadEnvMode } | null>
47 >;
48 readonly setPendingServerThreadStartFromOriginByThreadId: Dispatch<
49 SetStateAction<Record<string, boolean>>
50 >;
51 readonly scheduleComposerFocus: () => void;
53​
54/**
55 * The composer's choices for the next turn: provider and model, runtime mode,
56 * and where a new thread runs (local or a new worktree, from the remote's tip or not).
57 */
58export function useComposerSelection(input: ComposerSelectionInput) {
59 const {
60 environmentId,
61 composerDraftTarget,
62 draftThread,
63 activeThread,
64 activeRuntime,
65 activeProjectSettings,
66 settings,
67 providerStatuses,
68 lockedProvider,
69 supportsProviderSwitchingViaHandoff,
70 multipleModelSelections,
71 runtimeMode,
72 isLocalDraftThread,
73 canOverrideServerThreadEnvMode,
74 setPendingServerThreadEnvModeEntry,
75 setPendingServerThreadStartFromOriginByThreadId,
76 scheduleComposerFocus,
77 } = input;
78 const navigate = useNavigate();
79 const setStickyComposerModelSelection = useComposerDraftStore(
80 (store) => store.setStickyModelSelection,
81 );
82 const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection);
83 const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode);
84 const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
85 const applyThreadModelPick = useApplyThreadModelPick();
86​
87 const handleRuntimeModeChange = (mode: RuntimeMode) => {
88 if (mode === runtimeMode) return;
89 setComposerDraftRuntimeMode(composerDraftTarget, mode);
90 if (isLocalDraftThread) {
91 setDraftThreadContext(composerDraftTarget, { runtimeMode: mode });
92 }
93 scheduleComposerFocus();
94 };
95 const handleRuntimeModeChangeStable = useStableCallback(handleRuntimeModeChange);
96​
97 const openProviderSetup = (instanceId: ProviderInstanceId) => {
98 void navigate({
99 to: "/settings/providers",
100 search: { environmentId, instanceId },
101 });
102 };
103 const openProviderSetupStable = useStableCallback(openProviderSetup);
104​
105 // Keyed on the fields it reads: the thread and runtime objects change with
106 // every streamed event, and a new function re-renders the whole model picker.
107 const hasStartedSession = activeRuntime !== null;
108 const runtimeProviderInstanceId = activeRuntime?.providerInstanceId ?? null;
109 const currentModelSelection = activeThread?.modelSelection ?? null;
110 const getModelDisabledReason = makeModelDisabledReason(
111 currentModelSelection
112 ? {
113 hasStartedSession,
114 supportsProviderSwitchingViaHandoff,
115 currentModelSelection,
116 currentProviderInstanceId: runtimeProviderInstanceId,
117 }
118 : null,
119 );
120​
121 const onProviderModelSelect = (
122 instanceId: ProviderInstanceId,
123 model: string,
124 options?: { focusComposer?: boolean },
125 ) => {
126 if (!activeThread) return;
127 const entry = providerStatuses.find((snapshot) => snapshot.instanceId === instanceId);
128 const resolvedDriverKind = entry?.driver ?? null;
129 if (
130 !supportsProviderSwitchingViaHandoff &&
131 lockedProvider !== null &&
132 resolvedDriverKind !== null &&
133 resolvedDriverKind !== lockedProvider
134 ) {
135 if (options?.focusComposer !== false) scheduleComposerFocus();
136 return;
137 }
138 if (
139 !supportsProviderSwitchingViaHandoff &&
140 lockedProvider !== null &&
141 activeRuntime?.providerInstanceId
142 ) {
143 const currentEntry = providerStatuses.find(
144 (snapshot) => snapshot.instanceId === activeRuntime.providerInstanceId,
145 );
146 if (
147 currentEntry?.continuation?.groupKey &&
148 entry?.continuation?.groupKey &&
149 currentEntry.continuation.groupKey !== entry.continuation.groupKey
150 ) {
151 if (options?.focusComposer !== false) scheduleComposerFocus();
152 return;
153 }
154 }
155 // The picker lists an unavailable model only as the active selection, so re-picking keeps it.
156 const resolvedModel = resolveAppModelSelectionForInstance(
157 instanceId,
158 settings,
159 providerStatuses,
160 model,
161 { preserveUnavailableSelection: true },
162 );
163 const threadRef = scopeThreadRef(activeThread.environmentId, activeThread.id);
164 const draft = useComposerDraftStore.getState().getComposerDraft(threadRef);
165 const shownInstanceId =
166 draft?.activeProvider ??
167 activeRuntime?.providerInstanceId ??
168 activeThread.modelSelection.instanceId;
169 const shownSelection =
170 draft?.modelSelectionByProvider[shownInstanceId] ?? activeThread.modelSelection;
171 if (
172 !resolvedModel ||
173 (instanceId === shownInstanceId &&
174 shownSelection.instanceId === instanceId &&
175 shownSelection.model === resolvedModel)
176 ) {
177 // Re-picking the shown model keeps its options and saves it again if that save failed.
178 if (resolvedModel && !isLocalDraftThread) applyThreadModelPick(threadRef);
179 if (options?.focusComposer !== false) scheduleComposerFocus();
180 return;
181 }
182 const rememberedOptions =
183 useComposerDraftStore.getState().stickyOptionsByModelByProvider[instanceId]?.[resolvedModel];
184 const nextModelSelection: ModelSelection =
185 rememberedOptions !== undefined && rememberedOptions.length > 0
186 ? { instanceId, model: resolvedModel, options: [...rememberedOptions] }
187 : { instanceId, model: resolvedModel };
188 const modelChangeBlockReason = getStartedThreadModelChangeBlockReason({
189 hasStartedSession: activeRuntime !== null,
190 supportsProviderSwitchingViaHandoff,
191 currentModelSelection: activeThread.modelSelection,
192 currentProviderInstanceId: activeRuntime?.providerInstanceId ?? null,
193 nextModelSelection,
194 });
195 if (modelChangeBlockReason) {
196 toastManager.add({
197 type: "warning",
198 title: modelChangeBlockReason.title,
199 description: modelChangeBlockReason.description,
200 });
201 if (options?.focusComposer !== false) scheduleComposerFocus();
202 return;
203 }
204 setComposerDraftModelSelection(threadRef, nextModelSelection, {
205 explicit: true,
206 replaceOptions: true,
207 });
208 setStickyComposerModelSelection(nextModelSelection);
209 if (!isLocalDraftThread) applyThreadModelPick(threadRef);
210 if (options?.focusComposer !== false) scheduleComposerFocus();
211 };
212 const onProviderModelSelectStable = useStableCallback(onProviderModelSelect);
213 const onEnvModeChange = (mode: DraftThreadEnvMode) => {
214 if (multipleModelSelections !== null) return;
215 if (canOverrideServerThreadEnvMode) {
216 if (activeThread) setPendingServerThreadEnvModeEntry({ threadId: activeThread.id, mode });
217 scheduleComposerFocus();
218 return;
219 }
220 if (isLocalDraftThread) {
221 setDraftThreadContext(composerDraftTarget, {
222 envMode: mode,
223 startFromOrigin: resolveNewDraftStartFromOrigin({
224 envMode: mode,
225 newWorktreesStartFromOrigin: activeProjectSettings.settings.newWorktreesStartFromOrigin,
226 }),
227 ...(mode === "worktree" && draftThread?.worktreePath ? { worktreePath: null } : {}),
228 });
229 }
230 scheduleComposerFocus();
231 };
232 const onEnvModeChangeStable = useStableCallback(onEnvModeChange);
233​
234 // Reads only the id: the shell changes on every streamed event, and a new handler
235 // re-renders the branch toolbar and thread details panel with it.
236 const activeThreadId = activeThread?.id;
237 const onStartFromOriginChange = (nextStartFromOrigin: boolean) => {
238 if (canOverrideServerThreadEnvMode && activeThreadId !== undefined) {
239 setPendingServerThreadStartFromOriginByThreadId((current) =>
240 current[activeThreadId] === nextStartFromOrigin
241 ? current
242 : { ...current, [activeThreadId]: nextStartFromOrigin },
243 );
244 return;
245 }
246 if (isLocalDraftThread) {
247 setDraftThreadContext(composerDraftTarget, {
248 startFromOrigin: nextStartFromOrigin,
249 });
250 }
251 };
252 return {
253 getModelDisabledReason,
254 handleRuntimeModeChangeStable,
255 onEnvModeChangeStable,
256 onProviderModelSelectStable,
257 onStartFromOriginChange,
258 openProviderSetupStable,
259 };