@@ -523,21 +523,22 @@ func (m model) handleModelCommand(args string) (model, string) {
523523// picker calls this when a model from a non-active provider is chosen, so the
524524// picker can list every saved provider and switch across them (like a unified
525525// provider+model selector). The key is loaded from the encrypted store / env.
526- // The returned bool reports whether the switch actually committed — callers
527- // that branch on the outcome (the provider manager) must use it, never the
526+ // The returned bool reports whether the in-session switch committed — callers
527+ // that branch on that outcome (the provider manager) must use it, never the
528528// display text: UI copy is not a control-flow contract (a refusal quoting a
529529// provider name could contain any substring, and rewording the success line
530- // must not change behavior).
531- func (m model ) switchProviderModel (providerName , modelID string ) (model , string , bool , tea.Cmd ) {
530+ // must not change behavior). The returned error reports that the in-session
531+ // switch succeeded but its provider/model transaction was not durably saved.
532+ func (m model ) switchProviderModel (providerName , modelID string ) (model , string , bool , tea.Cmd , error ) {
532533 if m .pending {
533- return m , "Model\n Cannot switch providers while a run is active." , false , nil
534+ return m , "Model\n Cannot switch providers while a run is active." , false , nil , nil
534535 }
535536 if m .newProvider == nil {
536- return m , "Model\n Provider rebuild is not available for this TUI session." , false , nil
537+ return m , "Model\n Provider rebuild is not available for this TUI session." , false , nil , nil
537538 }
538539 target , ok := m .savedProviderByName (providerName )
539540 if ! ok {
540- return m , "Model\n unknown provider " + strconv .Quote (providerName ), false , nil
541+ return m , "Model\n unknown provider " + strconv .Quote (providerName ), false , nil , nil
541542 }
542543 previousProviderName := m .providerName
543544 previousModel := m .modelName
@@ -551,11 +552,11 @@ func (m model) switchProviderModel(providerName, modelID string) (model, string,
551552 // keyless on purpose so newProvider attaches the bearer resolver + login key.
552553 if strings .TrimSpace (target .APIKey ) == "" && strings .TrimSpace (target .AuthHeaderValue ) == "" &&
553554 (! hasDescriptor || ! descriptor .Local ) && ! oauthLoginAvailable (target ) {
554- return m , "Model\n provider " + strconv .Quote (providerName ) + " has no usable credential — run setup or `zero auth login " + providerName + "`." , false , nil
555+ return m , "Model\n provider " + strconv .Quote (providerName ) + " has no usable credential — run setup or `zero auth login " + providerName + "`." , false , nil , nil
555556 }
556557 next , err := m .newProvider (target )
557558 if err != nil {
558- return m , "Model\n " + redaction .RedactString (err .Error (), redaction.Options {ExtraSecretValues : []string {target .APIKey }}), false , nil
559+ return m , "Model\n " + redaction .RedactString (err .Error (), redaction.Options {ExtraSecretValues : []string {target .APIKey }}), false , nil , nil
559560 }
560561 m .provider = next
561562 m .providerProfile = target
@@ -569,20 +570,24 @@ func (m model) switchProviderModel(providerName, modelID string) (model, string,
569570 // carried (pre-existing behavior) while the profile's own fill stays
570571 // conservative — it only ever applies where support is known.
571572 m = m .reconcileProfileAfterModelSwitch (m .availableReasoningEfforts ())
572- // Record the outgoing pair too — see the matching comment in
573- // handleModelCommand for why (keeps the session's starting model from
574- // silently dropping out of "Recent" on the first switch away from it).
575- // recordRecentModels batches both into a single normalize+persist instead
576- // of two separate disk writes for this one switch.
577- m = m .recordRecentModels (
578- config.RecentModelEntry {Provider : previousProviderName , Model : previousModel },
579- config.RecentModelEntry {Provider : target .Name , Model : target .Model },
580- )
581573 // Keep sub-agent child processes on the same provider we just switched to.
582574 config .SetActiveProviderEnv (target .Name )
583- if strings .TrimSpace (m .userConfigPath ) != "" {
584- _ , _ = config .SetActiveProvider (m .userConfigPath , target .Name )
585- _ , _ = config .SetProviderModel (m .userConfigPath , target .Name , target .Model )
575+ path := strings .TrimSpace (m .userConfigPath )
576+ var persistErr error
577+ if path != "" {
578+ _ , persistErr = config .SetActiveProviderModel (path , target .Name , target .Model )
579+ }
580+ recent := []config.RecentModelEntry {
581+ {Provider : previousProviderName , Model : previousModel },
582+ {Provider : target .Name , Model : target .Model },
583+ }
584+ if persistErr == nil {
585+ // Record the outgoing pair too — see the matching comment in
586+ // handleModelCommand for why. Persist history only after the selection
587+ // transaction succeeds, so one unavailable lock causes one wait.
588+ m = m .recordRecentModels (recent ... )
589+ } else {
590+ m , _ = m .updateRecentModels (recent ... )
586591 }
587592 // Warm discovery for the provider we just switched to, same as Init() does
588593 // for the provider active at launch — otherwise the context-usage gauge has
@@ -597,10 +602,15 @@ func (m model) switchProviderModel(providerName, modelID string) (model, string,
597602 }
598603 }
599604 status := fmt .Sprintf ("Model\n Switched to %s · %s" , target .Name , target .Model )
605+ if path != "" && persistErr == nil {
606+ status += " · saved"
607+ } else if persistErr != nil {
608+ status += " · not saved (" + persistErr .Error () + ")"
609+ }
600610 if warn := m .visionDropWarning (); warn != "" {
601611 status += "\n " + warn
602612 }
603- return m , status , true , tea .Batch (cmds ... )
613+ return m , status , true , tea .Batch (cmds ... ), persistErr
604614}
605615
606616// profileWithCredential fills a profile's APIKey for provider construction the same
0 commit comments