Detailed changes
@@ -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
@@ -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
}
@@ -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)
@@ -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")
}
@@ -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")
}
@@ -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 {
@@ -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)
@@ -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
@@ -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
}
@@ -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")
}
@@ -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)
@@ -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,
@@ -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?")
@@ -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().
@@ -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.