refactor: remove Config() from App and Common, drop App.config field

Kujtim Hoxha created

Remove Config() accessor from App and Common. All callers now use
ConfigService() or ConfigService().Config() when passing *Config
to functions that still require it. Remove the config field from
App struct — only configService remains.

🐾 Generated with Crush

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

Change summary

internal/app/app.go             | 11 ++---------
internal/app/lsp.go             |  4 ++--
internal/ui/common/common.go    |  5 -----
internal/ui/dialog/commands.go  | 16 ++++++++--------
internal/ui/dialog/models.go    | 12 +++++++-----
internal/ui/dialog/reasoning.go |  8 ++++----
internal/ui/model/onboarding.go |  6 +++---
internal/ui/model/ui.go         | 18 +++++++++---------
8 files changed, 35 insertions(+), 45 deletions(-)

Detailed changes

internal/app/app.go 🔗

@@ -61,7 +61,6 @@ type App struct {
 	LSPClients *csync.Map[string, *lsp.Client]
 
 	configService *config.Service
-	config        *config.Config
 
 	serviceEventsWG *sync.WaitGroup
 	eventsCtx       context.Context
@@ -97,7 +96,6 @@ func New(ctx context.Context, conn *sql.DB, cfgSvc *config.Service) (*App, error
 		globalCtx: ctx,
 
 		configService: cfgSvc,
-		config:        cfg,
 
 		events:          make(chan tea.Msg, 100),
 		serviceEventsWG: &sync.WaitGroup{},
@@ -133,11 +131,6 @@ func (app *App) ConfigService() *config.Service {
 	return app.configService
 }
 
-// Config returns the application configuration.
-func (app *App) Config() *config.Config {
-	return app.config
-}
-
 // RunNonInteractive runs the application in non-interactive mode with the
 // given prompt, printing to stdout.
 func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool) error {
@@ -328,7 +321,7 @@ func (app *App) UpdateAgentModel(ctx context.Context) error {
 // If largeModel is provided but smallModel is not, the small model defaults to
 // the provider's default small model.
 func (app *App) overrideModelsForNonInteractive(ctx context.Context, largeModel, smallModel string) error {
-	providers := app.config.Providers.Copy()
+	providers := app.configService.Config().Providers.Copy()
 
 	largeMatches, smallMatches, err := findModels(providers, largeModel, smallModel)
 	if err != nil {
@@ -376,7 +369,7 @@ func (app *App) overrideModelsForNonInteractive(ctx context.Context, largeModel,
 // GetDefaultSmallModel returns the default small model for the given
 // provider. Falls back to the large model if no default is found.
 func (app *App) GetDefaultSmallModel(providerID string) config.SelectedModel {
-	cfg := app.config
+	cfg := app.configService.Config()
 	largeModelCfg := cfg.Models[config.SelectedModelTypeLarge]
 
 	// Find the provider in the known providers list to get its default small model.

internal/app/lsp.go 🔗

@@ -22,7 +22,7 @@ func (app *App) initLSPClients(ctx context.Context) {
 	manager.LoadDefaults()
 
 	var userConfiguredLSPs []string
-	for name, clientConfig := range app.config.LSP {
+	for name, clientConfig := range app.configService.LSP() {
 		if clientConfig.Disabled {
 			slog.Info("Skipping disabled LSP client", "name", name)
 			manager.RemoveServer(name)
@@ -70,7 +70,7 @@ func (app *App) initLSPClients(ctx context.Context) {
 		wg.Go(func() {
 			app.createAndStartLSPClient(
 				ctx, name,
-				toOurConfig(server, app.config.LSP[name]),
+				toOurConfig(server, app.configService.LSP()[name]),
 				slices.Contains(userConfiguredLSPs, name),
 			)
 		})

internal/ui/common/common.go 🔗

@@ -26,11 +26,6 @@ type Common struct {
 	Styles *styles.Styles
 }
 
-// Config returns the configuration associated with this [Common] instance.
-func (c *Common) Config() *config.Config {
-	return c.App.Config()
-}
-
 // ConfigService returns the config service associated with this [Common] instance.
 func (c *Common) ConfigService() *config.Service {
 	return c.App.ConfigService()

internal/ui/dialog/commands.go 🔗

@@ -396,12 +396,12 @@ func (c *Commands) defaultCommands() []*CommandItem {
 	}
 
 	// Add reasoning toggle for models that support it
-	cfg := c.com.Config()
-	if agentCfg, ok := cfg.Agents[config.AgentCoder]; ok {
-		providerCfg := c.com.ConfigService().GetProviderForModel(agentCfg.Model)
-		model := c.com.ConfigService().GetModelByType(agentCfg.Model)
+	svc := c.com.ConfigService()
+	if agentCfg, ok := svc.Agent(config.AgentCoder); ok {
+		providerCfg := svc.GetProviderForModel(agentCfg.Model)
+		model := svc.GetModelByType(agentCfg.Model)
 		if providerCfg != nil && model != nil && model.CanReason {
-			selectedModel := cfg.Models[agentCfg.Model]
+			selectedModel, _ := svc.SelectedModel(agentCfg.Model)
 
 			// Anthropic models: thinking toggle
 			if model.CanReason && len(model.ReasoningLevels) == 0 {
@@ -425,9 +425,9 @@ func (c *Commands) defaultCommands() []*CommandItem {
 		commands = append(commands, NewCommandItem(c.com.Styles, "toggle_sidebar", "Toggle Sidebar", "", ActionToggleCompactMode{}))
 	}
 	if c.sessionID != "" {
-		cfg := c.com.Config()
-		agentCfg := cfg.Agents[config.AgentCoder]
-		model := c.com.ConfigService().GetModelByType(agentCfg.Model)
+		svc := c.com.ConfigService()
+		agentCfg, _ := svc.Agent(config.AgentCoder)
+		model := svc.GetModelByType(agentCfg.Model)
 		if model != nil && model.SupportsImages {
 			commands = append(commands, NewCommandItem(c.com.Styles, "file_picker", "Open File Picker", "ctrl+f", ActionOpenDialog{
 				// TODO: Pass in the file picker dialog id

internal/ui/dialog/models.go 🔗

@@ -138,7 +138,7 @@ func NewModels(com *common.Common, isOnboarding bool) (*Models, error) {
 	)
 	m.keyMap.Close = CloseKey
 
-	providers, err := getFilteredProviders(com.Config())
+	providers, err := getFilteredProviders(com.ConfigService())
 	if err != nil {
 		return nil, fmt.Errorf("failed to get providers: %w", err)
 	}
@@ -335,12 +335,13 @@ func (m *Models) FullHelp() [][]key.Binding {
 // setProviderItems sets the provider items in the list.
 func (m *Models) setProviderItems() error {
 	t := m.com.Styles
-	cfg := m.com.Config()
+	svc := m.com.ConfigService()
+	cfg := svc.Config()
 
 	var selectedItemID string
 	selectedType := m.modelType.Config()
-	currentModel := cfg.Models[selectedType]
-	recentItems := cfg.RecentModels[selectedType]
+	currentModel, _ := svc.SelectedModel(selectedType)
+	recentItems := svc.RecentModels(selectedType)
 
 	// Track providers already added to avoid duplicates
 	addedProviders := make(map[string]bool)
@@ -505,7 +506,8 @@ func (m *Models) setProviderItems() error {
 	return nil
 }
 
-func getFilteredProviders(cfg *config.Config) ([]catwalk.Provider, error) {
+func getFilteredProviders(svc *config.Service) ([]catwalk.Provider, error) {
+	cfg := svc.Config()
 	providers, err := config.Providers(cfg)
 	if err != nil {
 		return nil, fmt.Errorf("failed to get providers: %w", err)

internal/ui/dialog/reasoning.go 🔗

@@ -220,14 +220,14 @@ func (r *Reasoning) FullHelp() [][]key.Binding {
 }
 
 func (r *Reasoning) setReasoningItems() error {
-	cfg := r.com.Config()
-	agentCfg, ok := cfg.Agents[config.AgentCoder]
+	svc := r.com.ConfigService()
+	agentCfg, ok := svc.Agent(config.AgentCoder)
 	if !ok {
 		return errors.New("agent configuration not found")
 	}
 
-	selectedModel := cfg.Models[agentCfg.Model]
-	model := r.com.ConfigService().GetModelByType(agentCfg.Model)
+	selectedModel, _ := svc.SelectedModel(agentCfg.Model)
+	model := svc.GetModelByType(agentCfg.Model)
 	if model == nil {
 		return errors.New("model configuration not found")
 	}

internal/ui/model/onboarding.go 🔗

@@ -19,7 +19,7 @@ import (
 // markProjectInitialized marks the current project as initialized in the config.
 func (m *UI) markProjectInitialized() tea.Msg {
 	// TODO: handle error so we show it in the tui footer
-	err := config.MarkProjectInitialized(m.com.Config())
+	err := config.MarkProjectInitialized(m.com.ConfigService().Config())
 	if err != nil {
 		slog.Error(err.Error())
 	}
@@ -52,7 +52,7 @@ func (m *UI) initializeProject() tea.Cmd {
 	if cmd := m.newSession(); cmd != nil {
 		cmds = append(cmds, cmd)
 	}
-	cfg := m.com.Config()
+	cfg := m.com.ConfigService().Config()
 
 	initialize := func() tea.Msg {
 		initPrompt, err := agent.InitializePrompt(*cfg)
@@ -77,7 +77,7 @@ func (m *UI) skipInitializeProject() tea.Cmd {
 
 // initializeView renders the project initialization prompt with Yes/No buttons.
 func (m *UI) initializeView() string {
-	cfg := m.com.Config()
+	cfg := m.com.ConfigService().Config()
 	s := m.com.Styles.Initialize
 	cwd := home.Short(m.com.ConfigService().WorkingDir())
 	initFile := cfg.Options.InitializeAs

internal/ui/model/ui.go 🔗

@@ -298,7 +298,7 @@ func New(com *common.Common) *UI {
 	desiredFocus := uiFocusEditor
 	if !com.ConfigService().IsConfigured() {
 		desiredState = uiOnboarding
-	} else if n, _ := config.ProjectNeedsInitialization(com.Config()); n {
+	} else if n, _ := config.ProjectNeedsInitialization(com.ConfigService().Config()); n {
 		desiredState = uiInitialize
 	}
 
@@ -345,7 +345,7 @@ func (m *UI) setState(state uiState, focus uiFocusState) {
 // loadCustomCommands loads the custom commands asynchronously.
 func (m *UI) loadCustomCommands() tea.Cmd {
 	return func() tea.Msg {
-		customCommands, err := commands.LoadCustomCommands(m.com.Config())
+		customCommands, err := commands.LoadCustomCommands(m.com.ConfigService().Config())
 		if err != nil {
 			slog.Error("Failed to load custom commands", "error", err)
 		}
@@ -776,7 +776,7 @@ func (m *UI) setSessionMessages(msgs []message.Message) tea.Cmd {
 		case message.Assistant:
 			items = append(items, chat.ExtractMessageItems(m.com.Styles, msg, toolResultMap)...)
 			if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
-				infoItem := chat.NewAssistantInfoItem(m.com.Styles, msg, m.com.Config(), time.Unix(m.lastUserMessageTime, 0))
+				infoItem := chat.NewAssistantInfoItem(m.com.Styles, msg, m.com.ConfigService().Config(), time.Unix(m.lastUserMessageTime, 0))
 				items = append(items, infoItem)
 			}
 		default:
@@ -906,7 +906,7 @@ func (m *UI) appendSessionMessage(msg message.Message) tea.Cmd {
 			}
 		}
 		if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
-			infoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, m.com.Config(), time.Unix(m.lastUserMessageTime, 0))
+			infoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, m.com.ConfigService().Config(), time.Unix(m.lastUserMessageTime, 0))
 			m.chat.AppendMessages(infoItem)
 			if atBottom {
 				if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
@@ -977,7 +977,7 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
 
 	if shouldRenderAssistant && msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
 		if infoItem := m.chat.MessageItem(chat.AssistantInfoID(msg.ID)); infoItem == nil {
-			newInfoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, m.com.Config(), time.Unix(m.lastUserMessageTime, 0))
+			newInfoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, m.com.ConfigService().Config(), time.Unix(m.lastUserMessageTime, 0))
 			m.chat.AppendMessages(newInfoItem)
 		}
 	}
@@ -1196,7 +1196,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
 		m.dialog.CloseDialog(dialog.CommandsID)
 	case dialog.ActionToggleThinking:
 		cmds = append(cmds, func() tea.Msg {
-			cfg := m.com.Config()
+			cfg := m.com.ConfigService().Config()
 			if cfg == nil {
 				return util.ReportError(errors.New("configuration not found"))()
 			}
@@ -1235,7 +1235,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
 			break
 		}
 
-		cfg := m.com.Config()
+		cfg := m.com.ConfigService().Config()
 		if cfg == nil {
 			cmds = append(cmds, util.ReportError(errors.New("configuration not found")))
 			break
@@ -1297,7 +1297,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
 			break
 		}
 
-		cfg := m.com.Config()
+		cfg := m.com.ConfigService().Config()
 		if cfg == nil {
 			cmds = append(cmds, util.ReportError(errors.New("configuration not found")))
 			break
@@ -3063,7 +3063,7 @@ func (m *UI) drawSessionDetails(scr uv.Screen, area uv.Rectangle) {
 
 func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string) tea.Cmd {
 	load := func() tea.Msg {
-		prompt, err := commands.GetMCPPrompt(m.com.Config(), clientID, promptID, arguments)
+		prompt, err := commands.GetMCPPrompt(m.com.ConfigService().Config(), clientID, promptID, arguments)
 		if err != nil {
 			// TODO: make this better
 			return util.ReportError(err)()