fix: protect providers map (#1268)

Mohamed Mahmoud created

## What?

Protected the `providers` map with a `sync.RWMutex` to fix a race
condition.

## Why?

`ensureProviders()` writes to the map on the Bubble Tea event loop while
`getProvider()` is called concurrently from goroutines spawned by batch
and search commands, causing unsafe concurrent map access.

Closes #766

Change summary

main.go | 20 +++++++++++++++++---
1 file changed, 17 insertions(+), 3 deletions(-)

Detailed changes

main.go 🔗

@@ -101,7 +101,8 @@ type mainModel struct {
 	idleWatcher *fetcher.IdleWatcher
 	idleUpdates chan fetcher.IdleUpdate
 	// Multi-protocol backend providers (keyed by account ID)
-	providers map[string]backend.Provider
+	providers   map[string]backend.Provider
+	providersMu sync.RWMutex
 	// Daemon client service (daemon or direct fallback)
 	service daemonclient.Service
 	// Plugin prompt waiting for user input
@@ -155,15 +156,23 @@ func (m *mainModel) ensureProviders() {
 		return
 	}
 	for _, acct := range m.config.Accounts {
-		if _, ok := m.providers[acct.ID]; ok {
+		m.providersMu.RLock()
+		_, ok := m.providers[acct.ID]
+		m.providersMu.RUnlock()
+
+		if ok {
 			continue
 		}
+
 		p, err := backend.New(&acct)
 		if err != nil {
 			log.Printf("backend: failed to create provider for %s: %v", acct.Email, err)
 			continue
 		}
+
+		m.providersMu.Lock()
 		m.providers[acct.ID] = p
+		m.providersMu.Unlock()
 	}
 }
 
@@ -172,7 +181,12 @@ func (m *mainModel) getProvider(acct *config.Account) backend.Provider {
 	if acct == nil {
 		return nil
 	}
-	return m.providers[acct.ID]
+
+	m.providersMu.RLock()
+	p := m.providers[acct.ID]
+	m.providersMu.RUnlock()
+
+	return p
 }
 
 func (m *mainModel) Init() tea.Cmd {