From f1c794f66a4213f8392d49cf96cc2065cdd82e08 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Fri, 6 Feb 2026 12:43:18 +0100 Subject: [PATCH] refactor: add read accessor methods to Service, migrate callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add WorkingDir, EnabledProviders, IsConfigured, GetModel, GetProviderForModel, GetModelByType, LargeModel, SmallModel, Resolve, and Resolver to Service. Migrate callers in coordinator, app, and UI to use ConfigService() instead of Config() for these methods. Config retains the methods for prompt.Build and internal load flow compatibility. 🐾 Generated with Crush Assisted-by: Claude Opus 4.6 via Crush --- internal/agent/coordinator.go | 32 ++++++++--------- internal/app/app.go | 4 +-- internal/app/lsp.go | 6 ++-- internal/cmd/models.go | 2 +- internal/cmd/run.go | 2 +- internal/config/service.go | 56 +++++++++++++++++++++++++++++ internal/ui/dialog/api_key_input.go | 2 +- internal/ui/dialog/commands.go | 6 ++-- internal/ui/dialog/filepicker.go | 2 +- internal/ui/dialog/reasoning.go | 2 +- internal/ui/model/header.go | 5 ++- internal/ui/model/landing.go | 2 +- internal/ui/model/onboarding.go | 2 +- internal/ui/model/sidebar.go | 4 +-- internal/ui/model/ui.go | 6 ++-- 15 files changed, 94 insertions(+), 39 deletions(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 97963fe9a728d0febb8da3ad0439fafba86babbe..83f5379081c47b06815e5e8b443c3a0b30f711f2 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -105,7 +105,7 @@ func NewCoordinator( } // TODO: make this dynamic when we support multiple agents - prompt, err := coderPrompt(prompt.WithWorkingDir(c.cfg.WorkingDir())) + prompt, err := coderPrompt(prompt.WithWorkingDir(c.cfgSvc.WorkingDir())) if err != nil { return nil, err } @@ -416,26 +416,26 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan // Get the model name for the agent modelName := "" if modelCfg, ok := c.cfg.Models[agent.Model]; ok { - if model := c.cfg.GetModel(modelCfg.Provider, modelCfg.Model); model != nil { + if model := c.cfgSvc.GetModel(modelCfg.Provider, modelCfg.Model); model != nil { modelName = model.Name } } allTools = append(allTools, - tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution, modelName), + tools.NewBashTool(c.permissions, c.cfgSvc.WorkingDir(), c.cfg.Options.Attribution, modelName), tools.NewJobOutputTool(), tools.NewJobKillTool(), - tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil), - tools.NewEditTool(c.lspClients, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()), - tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()), - tools.NewFetchTool(c.permissions, c.cfg.WorkingDir(), nil), - tools.NewGlobTool(c.cfg.WorkingDir()), - tools.NewGrepTool(c.cfg.WorkingDir()), - tools.NewLsTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Tools.Ls), + tools.NewDownloadTool(c.permissions, c.cfgSvc.WorkingDir(), nil), + tools.NewEditTool(c.lspClients, c.permissions, c.history, c.filetracker, c.cfgSvc.WorkingDir()), + tools.NewMultiEditTool(c.lspClients, c.permissions, c.history, c.filetracker, c.cfgSvc.WorkingDir()), + tools.NewFetchTool(c.permissions, c.cfgSvc.WorkingDir(), nil), + tools.NewGlobTool(c.cfgSvc.WorkingDir()), + tools.NewGrepTool(c.cfgSvc.WorkingDir()), + tools.NewLsTool(c.permissions, c.cfgSvc.WorkingDir(), c.cfg.Tools.Ls), tools.NewSourcegraphTool(nil), tools.NewTodosTool(c.sessions), - tools.NewViewTool(c.lspClients, c.permissions, c.filetracker, c.cfg.WorkingDir(), c.cfg.Options.SkillsPaths...), - tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()), + tools.NewViewTool(c.lspClients, c.permissions, c.filetracker, c.cfgSvc.WorkingDir(), c.cfg.Options.SkillsPaths...), + tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.filetracker, c.cfgSvc.WorkingDir()), ) if c.lspClients.Len() > 0 { @@ -449,7 +449,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan } } - for _, tool := range tools.GetMCPTools(c.permissions, c.cfg, c.cfg.WorkingDir()) { + for _, tool := range tools.GetMCPTools(c.permissions, c.cfg, c.cfgSvc.WorkingDir()) { if agent.AllowedMCP == nil { // No MCP restrictions filteredTools = append(filteredTools, tool) @@ -781,8 +781,8 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con } } - apiKey, _ := c.cfg.Resolve(providerCfg.APIKey) - baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL) + apiKey, _ := c.cfgSvc.Resolve(providerCfg.APIKey) + baseURL, _ := c.cfgSvc.Resolve(providerCfg.BaseURL) switch providerCfg.Type { case openai.Name: @@ -905,7 +905,7 @@ func (c *coordinator) refreshOAuth2Token(ctx context.Context, providerCfg config } func (c *coordinator) refreshApiKeyTemplate(ctx context.Context, providerCfg config.ProviderConfig) error { - newAPIKey, err := c.cfg.Resolve(providerCfg.APIKeyTemplate) + newAPIKey, err := c.cfgSvc.Resolve(providerCfg.APIKeyTemplate) if err != nil { slog.Error("Failed to re-resolve API key after 401 error", "provider", providerCfg.ID, "error", err) return err diff --git a/internal/app/app.go b/internal/app/app.go index 399e8a1abaf1ff59d8136e19daa64a50bfc1b81a..18bd3bed16ba3eb3f107e4c9d5dc98dbaead7f8c 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -90,7 +90,7 @@ func New(ctx context.Context, conn *sql.DB, cfgSvc *config.Service) (*App, error Sessions: sessions, Messages: messages, History: files, - Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools), + Permissions: permission.NewPermissionService(cfgSvc.WorkingDir(), skipPermissionsRequests, allowedTools), FileTracker: filetracker.NewService(q), LSPClients: csync.NewMap[string, *lsp.Client](), @@ -118,7 +118,7 @@ func New(ctx context.Context, conn *sql.DB, cfgSvc *config.Service) (*App, error app.cleanupFuncs = append(app.cleanupFuncs, conn.Close, mcp.Close) // TODO: remove the concept of agent config, most likely. - if !cfg.IsConfigured() { + if !cfgSvc.IsConfigured() { slog.Warn("No agent configuration found") return app, nil } diff --git a/internal/app/lsp.go b/internal/app/lsp.go index 2bb20fad3878a771ce8b6a2a4dc3688de44ba5dd..df6d8d86de63edee8c59c185e1938136d2785fcb 100644 --- a/internal/app/lsp.go +++ b/internal/app/lsp.go @@ -53,7 +53,7 @@ func (app *App) initLSPClients(ctx context.Context) { } servers := manager.GetServers() - filtered := lsp.FilterMatching(app.config.WorkingDir(), servers) + filtered := lsp.FilterMatching(app.configService.WorkingDir(), servers) for _, name := range userConfiguredLSPs { if _, ok := filtered[name]; !ok { @@ -114,7 +114,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config updateLSPState(name, lsp.StateStarting, nil, nil, 0) // Create LSP client. - lspClient, err := lsp.New(ctx, name, config, app.config.Resolver(), app.config.Options.DebugLSP) + lspClient, err := lsp.New(ctx, name, config, app.configService.Resolver(), app.config.Options.DebugLSP) if err != nil { if !userConfigured { slog.Warn("Default LSP config skipped due to error", "name", name, "error", err) @@ -134,7 +134,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config defer cancel() // Initialize LSP client. - _, err = lspClient.Initialize(initCtx, app.config.WorkingDir()) + _, err = lspClient.Initialize(initCtx, app.configService.WorkingDir()) if err != nil { slog.Error("LSP client initialization failed", "name", name, "error", err) updateLSPState(name, lsp.StateError, err, lspClient, 0) diff --git a/internal/cmd/models.go b/internal/cmd/models.go index f4fa559ebe41d93bee54ed5e2272f8fb0b8dc9ad..3b2c4f7443f5389171e69c85b96e08db3b4a0255 100644 --- a/internal/cmd/models.go +++ b/internal/cmd/models.go @@ -38,7 +38,7 @@ crush models gpt5`, return err } - if !cfg.Config().IsConfigured() { + if !cfg.IsConfigured() { return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively") } diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 50005a548bad0308bdca3a2afbe17503c1f86c56..5aa3198379178d4fb04dad46a5526a084073c332 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -50,7 +50,7 @@ crush run --verbose "Generate a README for this project" } defer app.Shutdown() - if !app.Config().IsConfigured() { + if !app.ConfigService().IsConfigured() { return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively") } diff --git a/internal/config/service.go b/internal/config/service.go index 2d595b5d919315d0bc2904566cf73d8717cb5bce..5a7aa0c03362a0ca4cb6a1b196b68a4ce31e0152 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -34,6 +34,62 @@ func (s *Service) Config() *Config { return s.cfg } +// WorkingDir returns the working directory. +func (s *Service) WorkingDir() string { + return s.workingDir +} + +// EnabledProviders returns all non-disabled provider configs. +func (s *Service) EnabledProviders() []ProviderConfig { + return s.cfg.EnabledProviders() +} + +// IsConfigured returns true if at least one provider is enabled. +func (s *Service) IsConfigured() bool { + return s.cfg.IsConfigured() +} + +// GetModel returns the catwalk model for the given provider and model +// ID, or nil if not found. +func (s *Service) GetModel(provider, model string) *catwalk.Model { + return s.cfg.GetModel(provider, model) +} + +// GetProviderForModel returns the provider config for the given model +// type, or nil. +func (s *Service) GetProviderForModel(modelType SelectedModelType) *ProviderConfig { + return s.cfg.GetProviderForModel(modelType) +} + +// GetModelByType returns the catwalk model for the given model type, +// or nil. +func (s *Service) GetModelByType(modelType SelectedModelType) *catwalk.Model { + return s.cfg.GetModelByType(modelType) +} + +// LargeModel returns the catwalk model for the large model type. +func (s *Service) LargeModel() *catwalk.Model { + return s.cfg.LargeModel() +} + +// SmallModel returns the catwalk model for the small model type. +func (s *Service) SmallModel() *catwalk.Model { + return s.cfg.SmallModel() +} + +// Resolve resolves a variable value using the configured resolver. +func (s *Service) Resolve(key string) (string, error) { + if s.resolver == nil { + return "", fmt.Errorf("no variable resolver configured") + } + return s.resolver.ResolveValue(key) +} + +// Resolver returns the variable resolver. +func (s *Service) Resolver() VariableResolver { + return s.resolver +} + // HasConfigField returns true if the given dotted key path exists in // the persisted config data. func (s *Service) HasConfigField(key string) bool { diff --git a/internal/ui/dialog/api_key_input.go b/internal/ui/dialog/api_key_input.go index a9df8bb39938481a7743ad243a54cabfee25c4e4..8bb25ba122321846d9a36e241353adbef13a0e45 100644 --- a/internal/ui/dialog/api_key_input.go +++ b/internal/ui/dialog/api_key_input.go @@ -296,7 +296,7 @@ func (m *APIKeyInput) verifyAPIKey() tea.Msg { Type: m.provider.Type, BaseURL: m.provider.APIEndpoint, } - err := providerConfig.TestConnection(m.com.Config().Resolver()) + err := providerConfig.TestConnection(m.com.ConfigService().Resolver()) // intentionally wait for at least 750ms to make sure the user sees the spinner elapsed := time.Since(start) diff --git a/internal/ui/dialog/commands.go b/internal/ui/dialog/commands.go index 0b0185b03a3c992ce55ff9164ceba6115260c174..47858bdb17914cd8105a24fe45fdb34399879619 100644 --- a/internal/ui/dialog/commands.go +++ b/internal/ui/dialog/commands.go @@ -398,8 +398,8 @@ 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 := cfg.GetProviderForModel(agentCfg.Model) - model := cfg.GetModelByType(agentCfg.Model) + providerCfg := c.com.ConfigService().GetProviderForModel(agentCfg.Model) + model := c.com.ConfigService().GetModelByType(agentCfg.Model) if providerCfg != nil && model != nil && model.CanReason { selectedModel := cfg.Models[agentCfg.Model] @@ -427,7 +427,7 @@ func (c *Commands) defaultCommands() []*CommandItem { if c.sessionID != "" { cfg := c.com.Config() agentCfg := cfg.Agents[config.AgentCoder] - model := cfg.GetModelByType(agentCfg.Model) + model := c.com.ConfigService().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 diff --git a/internal/ui/dialog/filepicker.go b/internal/ui/dialog/filepicker.go index 4b0b844e4ed869a4347af10e9d0b1b3c70a7d2f0..b9ae52393336a659ae52cb11317872975e2ccdba 100644 --- a/internal/ui/dialog/filepicker.go +++ b/internal/ui/dialog/filepicker.go @@ -123,7 +123,7 @@ func (f *FilePicker) SetImageCapabilities(caps *common.Capabilities) { // WorkingDir returns the current working directory of the [FilePicker]. func (f *FilePicker) WorkingDir() string { - wd := f.com.Config().WorkingDir() + wd := f.com.ConfigService().WorkingDir() if len(wd) > 0 { return wd } diff --git a/internal/ui/dialog/reasoning.go b/internal/ui/dialog/reasoning.go index 2a333f155cdc1499993f05411d7090793f74f54e..9017c5f0853a70b641a2fd6a66328e482df9e33e 100644 --- a/internal/ui/dialog/reasoning.go +++ b/internal/ui/dialog/reasoning.go @@ -227,7 +227,7 @@ func (r *Reasoning) setReasoningItems() error { } selectedModel := cfg.Models[agentCfg.Model] - model := cfg.GetModelByType(agentCfg.Model) + model := r.com.ConfigService().GetModelByType(agentCfg.Model) if model == nil { return errors.New("model configuration not found") } diff --git a/internal/ui/model/header.go b/internal/ui/model/header.go index 2f6e093027783dca62f3d6cde12d61126c6061bb..5af9d93a027f9da30ac4774260f1c8f2b5100940 100644 --- a/internal/ui/model/header.go +++ b/internal/ui/model/header.go @@ -118,7 +118,7 @@ func renderHeaderDetails( } agentCfg := com.Config().Agents[config.AgentCoder] - model := com.Config().GetModelByType(agentCfg.Model) + model := com.ConfigService().GetModelByType(agentCfg.Model) percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100 formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage))) parts = append(parts, formattedPercentage) @@ -135,8 +135,7 @@ func renderHeaderDetails( metadata = dot + metadata const dirTrimLimit = 4 - cfg := com.Config() - cwd := fsext.DirTrim(fsext.PrettyPath(cfg.WorkingDir()), dirTrimLimit) + cwd := fsext.DirTrim(fsext.PrettyPath(com.ConfigService().WorkingDir()), dirTrimLimit) cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…") cwd = t.Header.WorkingDir.Render(cwd) diff --git a/internal/ui/model/landing.go b/internal/ui/model/landing.go index a90ef76fdaf779e61477f5a05fd92a68d2e8a257..29f42f09044b39b80b1a28489ef7140ae9353d17 100644 --- a/internal/ui/model/landing.go +++ b/internal/ui/model/landing.go @@ -22,7 +22,7 @@ func (m *UI) selectedLargeModel() *agent.Model { func (m *UI) landingView() string { t := m.com.Styles width := m.layout.main.Dx() - cwd := common.PrettyPath(t, m.com.Config().WorkingDir(), width) + cwd := common.PrettyPath(t, m.com.ConfigService().WorkingDir(), width) parts := []string{ cwd, diff --git a/internal/ui/model/onboarding.go b/internal/ui/model/onboarding.go index 075067d75333fc539152f0041b4e5a3c2eed1c5e..9207f445c0b988c8c19a49a56c7889bd8c61d63e 100644 --- a/internal/ui/model/onboarding.go +++ b/internal/ui/model/onboarding.go @@ -79,7 +79,7 @@ func (m *UI) skipInitializeProject() tea.Cmd { func (m *UI) initializeView() string { cfg := m.com.Config() s := m.com.Styles.Initialize - cwd := home.Short(cfg.WorkingDir()) + cwd := home.Short(m.com.ConfigService().WorkingDir()) initFile := cfg.Options.InitializeAs header := s.Header.Render("Would you like to initialize this project?") diff --git a/internal/ui/model/sidebar.go b/internal/ui/model/sidebar.go index c3498b964ca5ebbc2446ffc31855a1c225a7ab5e..e4107ceca26e851d9cf18be19e944b0f27db4d69 100644 --- a/internal/ui/model/sidebar.go +++ b/internal/ui/model/sidebar.go @@ -114,7 +114,7 @@ func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) { height := area.Dy() title := t.Muted.Width(width).MaxHeight(2).Render(m.session.Title) - cwd := common.PrettyPath(t, m.com.Config().WorkingDir(), width) + cwd := common.PrettyPath(t, m.com.ConfigService().WorkingDir(), width) sidebarLogo := m.sidebarLogo if height < logoHeightBreakpoint { sidebarLogo = logo.SmallRender(m.com.Styles, width) @@ -140,7 +140,7 @@ func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) { lspSection := m.lspInfo(width, maxLSPs, true) mcpSection := m.mcpInfo(width, maxMCPs, true) - filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles, true) + filesSection := m.filesInfo(m.com.ConfigService().WorkingDir(), width, maxFiles, true) uv.NewStyledString( lipgloss.NewStyle(). diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 3bb552555b457e19a71f935a9ad7ea985f667857..7526c4897403c2fb3b05e97f1f6783c6d6470fa4 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -296,7 +296,7 @@ func New(com *common.Common) *UI { desiredState := uiLanding desiredFocus := uiFocusEditor - if !com.Config().IsConfigured() { + if !com.ConfigService().IsConfigured() { desiredState = uiOnboarding } else if n, _ := config.ProjectNeedsInitialization(com.Config()); n { desiredState = uiInitialize @@ -1920,7 +1920,7 @@ func (m *UI) View() tea.View { v.BackgroundColor = m.com.Styles.Background } v.MouseMode = tea.MouseModeCellMotion - v.WindowTitle = "crush " + home.Short(m.com.Config().WorkingDir()) + v.WindowTitle = "crush " + home.Short(m.com.ConfigService().WorkingDir()) canvas := uv.NewScreenBuffer(m.width, m.height) v.Cursor = m.Draw(canvas, canvas.Bounds()) @@ -3045,7 +3045,7 @@ func (m *UI) drawSessionDetails(scr uv.Screen, area uv.Rectangle) { lspSection := m.lspInfo(sectionWidth, maxItemsPerSection, false) mcpSection := m.mcpInfo(sectionWidth, maxItemsPerSection, false) - filesSection := m.filesInfo(m.com.Config().WorkingDir(), sectionWidth, maxItemsPerSection, false) + filesSection := m.filesInfo(m.com.ConfigService().WorkingDir(), sectionWidth, maxItemsPerSection, false) sections := lipgloss.JoinHorizontal(lipgloss.Top, filesSection, " ", lspSection, " ", mcpSection) uv.NewStyledString( s.CompactDetails.View.