refactor: move SetupAgents from Config to Service

Kujtim Hoxha created

Agents are now owned by Service. SetupAgents() builds agents on
the Service and syncs to Config.Agents for backward compatibility.

🐾 Generated with Crush

Assisted-by: Claude Opus 4.6 via Crush <crush@charm.land>

Change summary

internal/config/config.go    | 27 ---------------------------
internal/config/load.go      |  2 +-
internal/config/load_test.go |  6 +++---
internal/config/service.go   | 34 ++++++++++++++++++++++++++++++++++
internal/ui/model/ui.go      |  2 +-
5 files changed, 39 insertions(+), 32 deletions(-)

Detailed changes

internal/config/config.go 🔗

@@ -526,33 +526,6 @@ func filterSlice(data []string, mask []string, include bool) []string {
 	return filtered
 }
 
-func (c *Config) SetupAgents() {
-	allowedTools := resolveAllowedTools(allToolNames(), c.Options.DisabledTools)
-
-	agents := map[string]Agent{
-		AgentCoder: {
-			ID:           AgentCoder,
-			Name:         "Coder",
-			Description:  "An agent that helps with executing coding tasks.",
-			Model:        SelectedModelTypeLarge,
-			ContextPaths: c.Options.ContextPaths,
-			AllowedTools: allowedTools,
-		},
-
-		AgentTask: {
-			ID:           AgentCoder,
-			Name:         "Task",
-			Description:  "An agent that helps with searching for context and finding implementation details.",
-			Model:        SelectedModelTypeLarge,
-			ContextPaths: c.Options.ContextPaths,
-			AllowedTools: resolveReadOnlyTools(allowedTools),
-			// NO MCPs or LSPs by default
-			AllowedMCP: map[string][]string{},
-		},
-	}
-	c.Agents = agents
-}
-
 func (c *Config) Resolver() VariableResolver {
 	return c.resolver
 }

internal/config/load.go 🔗

@@ -101,7 +101,7 @@ func Load(workingDir, dataDir string, debug bool) (*Service, error) {
 	if err := svc.configureSelectedModels(svc.knownProviders); err != nil {
 		return nil, fmt.Errorf("failed to configure selected models: %w", err)
 	}
-	cfg.SetupAgents()
+	svc.SetupAgents()
 	return svc, nil
 }
 

internal/config/load_test.go 🔗

@@ -465,7 +465,7 @@ func TestConfig_setupAgentsWithNoDisabledTools(t *testing.T) {
 		},
 	}
 
-	cfg.SetupAgents()
+	serviceFor(cfg).SetupAgents()
 	coderAgent, ok := cfg.Agents[AgentCoder]
 	require.True(t, ok)
 	assert.Equal(t, allToolNames(), coderAgent.AllowedTools)
@@ -486,7 +486,7 @@ func TestConfig_setupAgentsWithDisabledTools(t *testing.T) {
 		},
 	}
 
-	cfg.SetupAgents()
+	serviceFor(cfg).SetupAgents()
 	coderAgent, ok := cfg.Agents[AgentCoder]
 	require.True(t, ok)
 
@@ -510,7 +510,7 @@ func TestConfig_setupAgentsWithEveryReadOnlyToolDisabled(t *testing.T) {
 		},
 	}
 
-	cfg.SetupAgents()
+	serviceFor(cfg).SetupAgents()
 	coderAgent, ok := cfg.Agents[AgentCoder]
 	require.True(t, ok)
 	assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "todos", "write"}, coderAgent.AllowedTools)

internal/config/service.go 🔗

@@ -25,6 +25,7 @@ type Service struct {
 	resolver       VariableResolver
 	workingDir     string
 	knownProviders []catwalk.Provider
+	agents         map[string]Agent
 }
 
 // Config returns the underlying Config struct. This is a temporary
@@ -90,6 +91,39 @@ func (s *Service) Resolver() VariableResolver {
 	return s.resolver
 }
 
+// SetupAgents builds the agent configurations from the current config
+// options.
+func (s *Service) SetupAgents() {
+	allowedTools := resolveAllowedTools(allToolNames(), s.cfg.Options.DisabledTools)
+
+	s.agents = map[string]Agent{
+		AgentCoder: {
+			ID:           AgentCoder,
+			Name:         "Coder",
+			Description:  "An agent that helps with executing coding tasks.",
+			Model:        SelectedModelTypeLarge,
+			ContextPaths: s.cfg.Options.ContextPaths,
+			AllowedTools: allowedTools,
+		},
+
+		AgentTask: {
+			ID:           AgentCoder,
+			Name:         "Task",
+			Description:  "An agent that helps with searching for context and finding implementation details.",
+			Model:        SelectedModelTypeLarge,
+			ContextPaths: s.cfg.Options.ContextPaths,
+			AllowedTools: resolveReadOnlyTools(allowedTools),
+			AllowedMCP:   map[string][]string{},
+		},
+	}
+	s.cfg.Agents = s.agents
+}
+
+// Agents returns the agent configuration map.
+func (s *Service) Agents() map[string]Agent {
+	return s.agents
+}
+
 // HasConfigField returns true if the given dotted key path exists in
 // the persisted config data.
 func (s *Service) HasConfigField(key string) bool {

internal/ui/model/ui.go 🔗

@@ -1286,7 +1286,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
 
 		if isOnboarding {
 			m.setState(uiLanding, uiFocusEditor)
-			m.com.Config().SetupAgents()
+			m.com.ConfigService().SetupAgents()
 			if err := m.com.App.InitCoderAgent(context.TODO()); err != nil {
 				cmds = append(cmds, util.ReportError(err))
 			}