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.