@@ -12,7 +12,7 @@ let _dummyDeleteSession: Types.deleteSessionFn = (_, ~onComplete as _) => ()
1212let _apiBaseUrl = "http://localhost:4000"
1313
1414// Helper: base state with an active ACP session (needed to emit effects)
15- let _makeState = (~anthropicOAuthStatus = Types .NotConnected , ~chatgptOAuthStatus = Types .ChatGPTNotConnected , ~openrouterKeySettings = {Types .source : Types .None , saveStatus : Types .Idle }): Types .state => {
15+ let _makeState = (~anthropicOAuthStatus = Types .NotConnected , ~chatgptOAuthStatus = Types .ChatGPTNotConnected , ~openrouterKeySettings = {Types .source : Types .None , saveStatus : Types .Idle }, ~ selectedModel = None , ~ pendingProviderAutoSelect = None ): Types .state => {
1616 {
1717 tasks : Dict .make (),
1818 currentTask : Types .Task .New (Types .Task .makeNew (~previewUrl = "http://localhost:3000" )),
@@ -30,7 +30,8 @@ let _makeState = (~anthropicOAuthStatus=Types.NotConnected, ~chatgptOAuthStatus=
3030 anthropicOAuthStatus ,
3131 chatgptOAuthStatus ,
3232 modelsConfig : None ,
33- selectedModel : None ,
33+ selectedModel ,
34+ pendingProviderAutoSelect ,
3435 sessionsLoadState : Types .SessionsNotLoaded ,
3536 }
3637}
@@ -137,3 +138,186 @@ describe("Models list refresh requires active ACP session", () => {
137138 t -> expect (_hasFetchModelsEffect (effects ))-> Expect .toBe (false )
138139 })
139140})
141+
142+ // ============================================================================
143+ // Auto-select default model from newly connected provider
144+ // ============================================================================
145+
146+ module SampleConfig = {
147+ let anthropicProvider : Types .providerConfig = {
148+ id : "anthropic" ,
149+ name : "Anthropic (Claude Pro/Max)" ,
150+ models : [
151+ {displayName : "Claude Sonnet 4.5" , value : "claude-sonnet-4-5" },
152+ {displayName : "Claude Opus 4.5" , value : "claude-opus-4-5" },
153+ ],
154+ }
155+ let openaiProvider : Types .providerConfig = {
156+ id : "openai" ,
157+ name : "ChatGPT Pro/Plus" ,
158+ models : [
159+ {displayName : "GPT-5.1 Codex Max" , value : "gpt-5.1-codex-max" },
160+ {displayName : "GPT-5.2" , value : "gpt-5.2" },
161+ ],
162+ }
163+ let openrouterProvider : Types .providerConfig = {
164+ id : "openrouter" ,
165+ name : "OpenRouter" ,
166+ models : [
167+ {displayName : "Gemini 3 Flash Preview" , value : "google/gemini-3-flash-preview" },
168+ {displayName : "Claude Haiku 4.5" , value : "anthropic/claude-haiku-4.5" },
169+ ],
170+ }
171+ let configWithAnthropic : Types .modelsConfig = {
172+ providers : [anthropicProvider , openrouterProvider ],
173+ defaultModel : {provider : "anthropic" , value : "claude-sonnet-4-5" },
174+ }
175+ let configWithOpenAI : Types .modelsConfig = {
176+ providers : [openaiProvider , anthropicProvider , openrouterProvider ],
177+ defaultModel : {provider : "openai" , value : "gpt-5.1-codex-max" },
178+ }
179+ let configWithOpenRouterOnly : Types .modelsConfig = {
180+ providers : [openrouterProvider ],
181+ defaultModel : {provider : "openrouter" , value : "google/gemini-3-flash-preview" },
182+ }
183+ }
184+
185+ describe ("Provider connect sets pendingProviderAutoSelect" , () => {
186+ test ("AnthropicOAuthConnected sets pendingProviderAutoSelect to anthropic" , t => {
187+ let state = _makeState ()
188+
189+ let (nextState , _effects ) = Reducer .next (
190+ state ,
191+ AnthropicOAuthConnected ({expiresAt : "2026-12-31T00:00:00Z" }),
192+ )
193+
194+ t -> expect (nextState .pendingProviderAutoSelect )-> Expect .toEqual (Some ("anthropic" ))
195+ })
196+
197+ test ("ChatGPTOAuthConnected sets pendingProviderAutoSelect to openai" , t => {
198+ let state = _makeState (
199+ ~chatgptOAuthStatus = Types .ChatGPTShowingCode ({
200+ deviceAuthId : "device-1" ,
201+ userCode : "ABCD-1234" ,
202+ verificationUrl : "https://auth.openai.com/codex/device" ,
203+ }),
204+ )
205+
206+ let (nextState , _effects ) = Reducer .next (
207+ state ,
208+ ChatGPTOAuthConnected ({deviceAuthId : "device-1" , expiresAt : "2026-12-31T00:00:00Z" }),
209+ )
210+
211+ t -> expect (nextState .pendingProviderAutoSelect )-> Expect .toEqual (Some ("openai" ))
212+ })
213+
214+ test ("OpenRouterKeySaved sets pendingProviderAutoSelect to openrouter" , t => {
215+ let state = _makeState ()
216+
217+ let (nextState , _effects ) = Reducer .next (state , OpenRouterKeySaved )
218+
219+ t -> expect (nextState .pendingProviderAutoSelect )-> Expect .toEqual (Some ("openrouter" ))
220+ })
221+ })
222+
223+ describe ("ModelsConfigReceived auto-selects model from newly connected provider" , () => {
224+ test ("auto-selects first Anthropic model when pendingProviderAutoSelect is anthropic" , t => {
225+ let state = _makeState (
226+ ~pendingProviderAutoSelect = Some ("anthropic" ),
227+ ~selectedModel = Some ({provider : "openrouter" , value : "google/gemini-3-flash-preview" }),
228+ )
229+
230+ let (nextState , _effects ) = Reducer .next (
231+ state ,
232+ ModelsConfigReceived ({config : SampleConfig .configWithAnthropic }),
233+ )
234+
235+ t
236+ -> expect (nextState .selectedModel )
237+ -> Expect .toEqual (Some ({provider : "anthropic" , value : "claude-sonnet-4-5" }))
238+ t -> expect (nextState .pendingProviderAutoSelect )-> Expect .toEqual (None )
239+ })
240+
241+ test ("auto-selects first OpenAI model when pendingProviderAutoSelect is openai" , t => {
242+ let state = _makeState (
243+ ~pendingProviderAutoSelect = Some ("openai" ),
244+ ~selectedModel = Some ({provider : "openrouter" , value : "google/gemini-3-flash-preview" }),
245+ )
246+
247+ let (nextState , _effects ) = Reducer .next (
248+ state ,
249+ ModelsConfigReceived ({config : SampleConfig .configWithOpenAI }),
250+ )
251+
252+ t
253+ -> expect (nextState .selectedModel )
254+ -> Expect .toEqual (Some ({provider : "openai" , value : "gpt-5.1-codex-max" }))
255+ t -> expect (nextState .pendingProviderAutoSelect )-> Expect .toEqual (None )
256+ })
257+
258+ test ("auto-selects first OpenRouter model when pendingProviderAutoSelect is openrouter" , t => {
259+ let state = _makeState (
260+ ~pendingProviderAutoSelect = Some ("openrouter" ),
261+ ~selectedModel = Some ({provider : "openrouter" , value : "anthropic/claude-haiku-4.5" }),
262+ )
263+
264+ let (nextState , _effects ) = Reducer .next (
265+ state ,
266+ ModelsConfigReceived ({config : SampleConfig .configWithOpenRouterOnly }),
267+ )
268+
269+ t
270+ -> expect (nextState .selectedModel )
271+ -> Expect .toEqual (Some ({provider : "openrouter" , value : "google/gemini-3-flash-preview" }))
272+ t -> expect (nextState .pendingProviderAutoSelect )-> Expect .toEqual (None )
273+ })
274+
275+ test ("keeps current selection when no pending provider auto-select" , t => {
276+ let existingModel : Types .selectedModel = {
277+ provider : "openrouter" ,
278+ value : "google/gemini-3-flash-preview" ,
279+ }
280+ let state = _makeState (~selectedModel = Some (existingModel ))
281+
282+ let (nextState , _effects ) = Reducer .next (
283+ state ,
284+ ModelsConfigReceived ({config : SampleConfig .configWithAnthropic }),
285+ )
286+
287+ t -> expect (nextState .selectedModel )-> Expect .toEqual (Some (existingModel ))
288+ t -> expect (nextState .pendingProviderAutoSelect )-> Expect .toEqual (None )
289+ })
290+
291+ test ("falls back to server default when no selection and no pending provider" , t => {
292+ let state = _makeState ()
293+
294+ let (nextState , _effects ) = Reducer .next (
295+ state ,
296+ ModelsConfigReceived ({config : SampleConfig .configWithAnthropic }),
297+ )
298+
299+ t
300+ -> expect (nextState .selectedModel )
301+ -> Expect .toEqual (Some ({provider : "anthropic" , value : "claude-sonnet-4-5" }))
302+ })
303+
304+ test ("clears pendingProviderAutoSelect even if provider not in config" , t => {
305+ let existingModel : Types .selectedModel = {
306+ provider : "openrouter" ,
307+ value : "google/gemini-3-flash-preview" ,
308+ }
309+ let state = _makeState (
310+ ~pendingProviderAutoSelect = Some ("openai" ),
311+ ~selectedModel = Some (existingModel ),
312+ )
313+
314+ // Config doesn't have OpenAI provider — keep existing selection
315+ let (nextState , _effects ) = Reducer .next (
316+ state ,
317+ ModelsConfigReceived ({config : SampleConfig .configWithOpenRouterOnly }),
318+ )
319+
320+ t -> expect (nextState .selectedModel )-> Expect .toEqual (Some (existingModel ))
321+ t -> expect (nextState .pendingProviderAutoSelect )-> Expect .toEqual (None )
322+ })
323+ })
0 commit comments