1import {
2 type ModelCapabilities,
3 type ModelSelection,
4 type ProviderDriverKind,
5 type ProviderInstanceId,
6 type ProviderOptionSelection,
7 type ScopedThreadRef,
8 type ServerProviderModel,
9} from "@t3tools/contracts";
10import {
11 buildExplicitProviderOptionSelectionsFromDescriptors,
12 getProviderOptionCurrentValue,
13 getProviderOptionDescriptors,
14 isClaudeUltrathinkPrompt,
15 normalizeModelSlug,
16} from "@t3tools/shared/model";
17import type { ReactNode } from "react";
18
19import type { DraftId } from "../../composerDraftStore";
20import { getProviderModelCapabilities } from "../../providerModels";
21import type { ComposerControlSize } from "./ComposerControl";
22import { shouldRenderTraitsControls, TraitsMenuContent, TraitsPicker } from "./TraitsPicker";
23
24export type ComposerProviderStateInput = {
25 provider: ProviderDriverKind;
26 model: string;
27 models: ReadonlyArray<ServerProviderModel>;
28 promptInjectionState?: ComposerPromptInjectionState;
29 modelOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined;
30 planModeEnabled: boolean;
31};
32
33export type ComposerPromptInjectionState = "none" | "ultrathink";
34
35export type ComposerProviderState = {
36 provider: ProviderDriverKind;
37 promptEffort: string | null;
38 modelOptionsForDispatch: ReadonlyArray<ProviderOptionSelection> | undefined;
39 composerFrameClassName?: string;
40 composerSurfaceClassName?: string;
41 modelPickerIconClassName?: string;
42};
43
44type TraitsRenderInput = {
45 provider: ProviderDriverKind;
46 instanceId?: ProviderInstanceId;
47 threadRef?: ScopedThreadRef;
48 draftId?: DraftId;
49 model: string;
50 models: ReadonlyArray<ServerProviderModel>;
51 modelOptions: ReadonlyArray<ProviderOptionSelection> | undefined;
52 reportedModelSelection?: ModelSelection | null | undefined;
53 prompt: string;
54 onPromptChange: (prompt: string) => void;
55 planModeEnabled: boolean;
56 size?: ComposerControlSize;
57 hidden?: boolean;
58 triggerClassName?: string;
59 isComposerOwned?: boolean;
60};
61
62export function getComposerPromptInjectionState(prompt: string): ComposerPromptInjectionState {
63 return isClaudeUltrathinkPrompt(prompt) ? "ultrathink" : "none";
64}
65
66/**
67 * Cursor ACP can report `fastMode: true` as the provider default. T3 only
68 * treats Fast as selected when the user chose it (draft/sticky/settings).
69 * Otherwise inject an explicit `false` so new chats stay Normal and the
70 * send path can overwrite a prior Fast session — descriptor defaults are
71 * otherwise omitted by `buildExplicitProviderOptionSelectionsFromDescriptors`.
72 */
73export function withImplicitFastModeDefault(
74 caps: ModelCapabilities,
75 modelOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined,
76): ReadonlyArray<ProviderOptionSelection> | undefined {
77 const hasExplicitFastMode = modelOptions?.some((selection) => selection.id === "fastMode");
78 if (hasExplicitFastMode) {
79 return modelOptions ?? undefined;
80 }
81 const hasFastModeDescriptor = caps.optionDescriptors?.some(
82 (descriptor) => descriptor.type === "boolean" && descriptor.id === "fastMode",
83 );
84 if (!hasFastModeDescriptor) {
85 return modelOptions ?? undefined;
86 }
87 return [...(modelOptions ?? []), { id: "fastMode", value: false }];
88}
89
90function resolveComposerOptionSelections(
91 models: ReadonlyArray<ServerProviderModel>,
92 model: string,
93 provider: ProviderDriverKind,
94 modelOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined,
95 planModeEnabled: boolean,
96): {
97 caps: ModelCapabilities;
98 selections: ReadonlyArray<ProviderOptionSelection> | undefined;
99} {
100 const caps = getProviderModelCapabilities(models, model, provider, planModeEnabled);
101 return { caps, selections: withImplicitFastModeDefault(caps, modelOptions) };
102}
103
104export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState {
105 const {
106 provider,
107 model,
108 models,
109 modelOptions,
110 promptInjectionState = "none",
111 planModeEnabled,
112 } = input;
113 if (provider === "opencode") {
114 const normalizedModel = normalizeModelSlug(model, provider);
115 const modelIsInCatalog = models.some((candidate) => candidate.slug === normalizedModel);
116 if (!modelIsInCatalog) {
117 const preservedOptions = modelOptions?.filter(
118 (option) => planModeEnabled || option.id !== "agent" || option.value !== "plan",
119 );
120 return {
121 provider,
122 promptEffort: null,
123 modelOptionsForDispatch:
124 preservedOptions && preservedOptions.length > 0 ? preservedOptions : undefined,
125 };
126 }
127 }
128 const { caps, selections } = resolveComposerOptionSelections(
129 models,
130 model,
131 provider,
132 modelOptions,
133 planModeEnabled,
134 );
135 const descriptors = getProviderOptionDescriptors({ caps, selections });
136 const primarySelectDescriptor = descriptors.find(
137 (descriptor): descriptor is Extract<(typeof descriptors)[number], { type: "select" }> =>
138 descriptor.type === "select",
139 );
140 const primaryValue = getProviderOptionCurrentValue(primarySelectDescriptor ?? null);
141 const promptEffort = typeof primaryValue === "string" ? primaryValue : null;
142 const ultrathinkActive =
143 (primarySelectDescriptor?.promptInjectedValues?.length ?? 0) > 0 &&
144 promptInjectionState === "ultrathink";
145
146 return {
147 provider,
148 promptEffort,
149 modelOptionsForDispatch: buildExplicitProviderOptionSelectionsFromDescriptors(
150 descriptors,
151 selections,
152 ),
153 ...(ultrathinkActive
154 ? {
155 composerFrameClassName: "ultrathink-frame",
156 composerSurfaceClassName: "shadow-[0_0_0_1px_rgba(255,255,255,0.07)_inset]",
157 modelPickerIconClassName: "ultrathink-chroma",
158 }
159 : {}),
160 };
161}
162
163function renderTraitsControl(
164 Component: typeof TraitsMenuContent | typeof TraitsPicker,
165 input: TraitsRenderInput,
166): ReactNode {
167 const {
168 provider,
169 instanceId,
170 threadRef,
171 draftId,
172 model,
173 models,
174 modelOptions,
175 reportedModelSelection,
176 prompt,
177 onPromptChange,
178 planModeEnabled,
179 size,
180 hidden,
181 triggerClassName,
182 isComposerOwned,
183 } = input;
184 const hasTarget = threadRef !== undefined || draftId !== undefined;
185 const { selections: resolvedModelOptions } = resolveComposerOptionSelections(
186 models,
187 model,
188 provider,
189 modelOptions,
190 planModeEnabled,
191 );
192 if (
193 !hasTarget ||
194 !shouldRenderTraitsControls({
195 provider,
196 models,
197 model,
198 modelOptions: resolvedModelOptions,
199 prompt,
200 planModeEnabled,
201 })
202 ) {
203 return null;
204 }
205 return (
206 <Component
207 provider={provider}
208 {...(instanceId ? { instanceId } : {})}
209 models={models}
210 {...(threadRef ? { threadRef } : {})}
211 {...(draftId ? { draftId } : {})}
212 model={model}
213 modelOptions={resolvedModelOptions}
214 reportedModelSelection={reportedModelSelection}
215 prompt={prompt}
216 onPromptChange={onPromptChange}
217 planModeEnabled={planModeEnabled}
218 {...(size !== undefined ? { size } : {})}
219 {...(hidden !== undefined ? { hidden } : {})}
220 {...(triggerClassName !== undefined ? { triggerClassName } : {})}
221 {...(isComposerOwned ? { isComposerOwned } : {})}
222 />
223 );
224}
225
226export function renderProviderTraitsMenuContent(input: TraitsRenderInput): ReactNode {
227 return renderTraitsControl(TraitsMenuContent, input);
228}
229
230export function renderProviderTraitsPicker(input: TraitsRenderInput): ReactNode {
231 return renderTraitsControl(TraitsPicker, input);
232}