@@ -10,8 +10,8 @@ import (
13+ "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/charmbracelet/crush/internal/logging"
14 "github.com/charmbracelet/crush/internal/logging"
14− "github.com/charmbracelet/fur/pkg/provider"
@@ -22,6 +22,29 @@ const (
22 MaxTokensFallbackDefault = 4096
22 MaxTokensFallbackDefault = 4096
25+var defaultContextPaths = []string{
26+ ".github/copilot-instructions.md",
42+ AgentCoder AgentID = "coder"
43+ AgentTask AgentID = "task"
44+ AgentTitle AgentID = "title"
45+ AgentSummarize AgentID = "summarize"
25type Model struct {
48type Model struct {
26 ID string `json:"id"`
49 ID string `json:"id"`
27 Name string `json:"model"`
50 Name string `json:"model"`
@@ -43,40 +66,43 @@ type VertexAIOptions struct {
45type ProviderConfig struct {
68type ProviderConfig struct {
69+ ID provider.InferenceProvider `json:"id"`
46− BaseURL string `json:"base_url,omitempty"`
70+ BaseURL string `json:"base_url,omitempty"`
47− ProviderType provider.Type `json:"provider_type"`
71+ ProviderType provider.Type `json:"provider_type"`
48− APIKey string `json:"api_key,omitempty"`
72+ APIKey string `json:"api_key,omitempty"`
49− Disabled bool `json:"disabled"`
73+ Disabled bool `json:"disabled"`
50− ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
74+ ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
51 // used for e.x for vertex to set the project
75 // used for e.x for vertex to set the project
52 ExtraParams map[string]string `json:"extra_params,omitempty"`
76 ExtraParams map[string]string `json:"extra_params,omitempty"`
54− DefaultModel string `json:"default_model"`
78+ DefaultLargeModel string `json:"default_large_model,omitempty"`
79+ DefaultSmallModel string `json:"default_small_model,omitempty"`
81+ Models []Model `json:"models,omitempty"`
57type Agent struct {
84type Agent struct {
58− Name string `json:"name"`
85+ Name string `json:"name"`
86+ Description string `json:"description,omitempty"`
59 // This is the id of the system prompt used by the agent
87 // This is the id of the system prompt used by the agent
60− // TODO: still needs to be implemented
61− PromptID string `json:"prompt_id"`
62− Disabled bool `json:"disabled"`
88+ Disabled bool `json:"disabled"`
64 Provider provider.InferenceProvider `json:"provider"`
90 Provider provider.InferenceProvider `json:"provider"`
65− Model Model `json:"model"`
91+ Model string `json:"model"`
67 // The available tools for the agent
93 // The available tools for the agent
68− // if this is empty, all tools are available
94+ // if this is nil, all tools are available
69 AllowedTools []string `json:"allowed_tools"`
95 AllowedTools []string `json:"allowed_tools"`
71 // this tells us which MCPs are available for this agent
97 // this tells us which MCPs are available for this agent
72 // if this is empty all mcps are available
98 // if this is empty all mcps are available
73− // the string array is the list of tools from the MCP the agent has available
99+ // the string array is the list of tools from the AllowedMCP the agent has available
74− // if the string array is empty, all tools from the MCP are available
100+ // if the string array is nil, all tools from the AllowedMCP are available
75− MCP map[string][]string `json:"mcp"`
101+ AllowedMCP map[string][]string `json:"allowed_mcp"`
77 // The list of LSPs that this agent can use
103 // The list of LSPs that this agent can use
78− // if this is empty, all LSPs are available
104+ // if this is nil, all LSPs are available
79− LSP []string `json:"lsp"`
105+ AllowedLSP []string `json:"allowed_lsp"`
81 // Overrides the context paths for this agent
107 // Overrides the context paths for this agent
82 ContextPaths []string `json:"context_paths"`
108 ContextPaths []string `json:"context_paths"`
@@ -125,7 +151,7 @@ type Config struct {
125 Providers map[provider.InferenceProvider]ProviderConfig `json:"providers,omitempty"`
151 Providers map[provider.InferenceProvider]ProviderConfig `json:"providers,omitempty"`
127 // List of configured agents
153 // List of configured agents
128− Agents map[string]Agent `json:"agents,omitempty"`
154+ Agents map[AgentID]Agent `json:"agents,omitempty"`
130 // List of configured MCPs
156 // List of configured MCPs
131 MCP map[string]MCP `json:"mcp,omitempty"`
157 MCP map[string]MCP `json:"mcp,omitempty"`
@@ -135,15 +161,13 @@ type Config struct {
136 // Miscellaneous options
162 // Miscellaneous options
137 Options Options `json:"options"`
163 Options Options `json:"options"`
139− // Used to add models that are not already in the repository
140− Models map[provider.InferenceProvider][]provider.Model `json:"models,omitempty"`
144 instance *Config // The single instance of the Singleton
167 instance *Config // The single instance of the Singleton
145 cwd string
168 cwd string
146 once sync.Once // Ensures the initialization happens only once
169 once sync.Once // Ensures the initialization happens only once
149func loadConfig(cwd string) (*Config, error) {
173func loadConfig(cwd string) (*Config, error) {
@@ -190,10 +214,73 @@ func loadConfig(cwd string) (*Config, error) {
192 // merge options
216 // merge options
193− cfg.Options = mergeOptions(cfg.Options, globalCfg.Options)
217+ mergeOptions(cfg, globalCfg, localConfig)
194− cfg.Options = mergeOptions(cfg.Options, localConfig.Options)
196 mergeProviderConfigs(cfg, globalCfg, localConfig)
219 mergeProviderConfigs(cfg, globalCfg, localConfig)
220+ // no providers found the app is not initialized yet
221+ if len(cfg.Providers) == 0 {
224+ preferredProvider := getPreferredProvider(cfg.Providers)
226+ if preferredProvider == nil {
227+ return nil, errors.New("no valid providers configured")
230+ agents := map[AgentID]Agent{
233+ Description: "An agent that helps with executing coding tasks.",
234+ Provider: preferredProvider.ID,
235+ Model: preferredProvider.DefaultLargeModel,
236+ ContextPaths: cfg.Options.ContextPaths,
237+ // All tools allowed
241+ Description: "An agent that helps with searching for context and finding implementation details.",
242+ Provider: preferredProvider.ID,
243+ Model: preferredProvider.DefaultLargeModel,
244+ ContextPaths: cfg.Options.ContextPaths,
245+ AllowedTools: []string{
252+ // NO MCPs or LSPs by default
253+ AllowedMCP: map[string][]string{},
254+ AllowedLSP: []string{},
258+ Description: "An agent that helps with generating titles for sessions.",
259+ Provider: preferredProvider.ID,
260+ Model: preferredProvider.DefaultSmallModel,
261+ ContextPaths: cfg.Options.ContextPaths,
262+ AllowedTools: []string{},
263+ // NO MCPs or LSPs by default
264+ AllowedMCP: map[string][]string{},
265+ AllowedLSP: []string{},
269+ Description: "An agent that helps with summarizing sessions.",
270+ Provider: preferredProvider.ID,
271+ Model: preferredProvider.DefaultSmallModel,
272+ ContextPaths: cfg.Options.ContextPaths,
273+ AllowedTools: []string{},
274+ // NO MCPs or LSPs by default
275+ AllowedMCP: map[string][]string{},
276+ AllowedLSP: []string{},
280+ mergeAgents(cfg, globalCfg, localConfig)
281+ mergeMCPs(cfg, globalCfg, localConfig)
282+ mergeLSPs(cfg, globalCfg, localConfig)
197 return cfg, nil
284 return cfg, nil
@@ -219,6 +306,22 @@ func GetConfig() *Config {
219 return instance
306 return instance
309+func getPreferredProvider(configuredProviders map[provider.InferenceProvider]ProviderConfig) *ProviderConfig {
310+ providers := Providers()
311+ for _, p := range providers {
312+ if providerConfig, ok := configuredProviders[p.ID]; ok && !providerConfig.Disabled {
313+ return &providerConfig
316+ // if none found return the first configured provider
317+ for _, providerConfig := range configuredProviders {
318+ if !providerConfig.Disabled {
319+ return &providerConfig
222func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfig) ProviderConfig {
325func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfig) ProviderConfig {
223 if other.APIKey != "" {
326 if other.APIKey != "" {
224 base.APIKey = other.APIKey
327 base.APIKey = other.APIKey
@@ -249,6 +352,26 @@ func mergeProviderConfig(p provider.InferenceProvider, base, other ProviderConfi
249 base.Disabled = other.Disabled
352 base.Disabled = other.Disabled
355+ if other.DefaultLargeModel != "" {
356+ base.DefaultLargeModel = other.DefaultLargeModel
358+ // Add new models if they don't exist
359+ if other.Models != nil {
360+ for _, model := range other.Models {
361+ // check if the model already exists
363+ for _, existingModel := range base.Models {
364+ if existingModel.ID == model.ID {
370+ base.Models = append(base.Models, model)
252 return base
375 return base
@@ -267,39 +390,114 @@ func validateProvider(p provider.InferenceProvider, providerConfig ProviderConfi
267 return nil
390 return nil
270−func mergeOptions(base, other Options) Options {
393+func mergeOptions(base, global, local *Config) {
271− result := base
394+ for _, cfg := range []*Config{global, local} {
398+ baseOptions := base.Options
399+ other := cfg.Options
400+ if len(other.ContextPaths) > 0 {
401+ baseOptions.ContextPaths = append(baseOptions.ContextPaths, other.ContextPaths...)
273− if len(other.ContextPaths) > 0 {
404+ if other.TUI.CompactMode {
274− base.ContextPaths = append(base.ContextPaths, other.ContextPaths...)
405+ baseOptions.TUI.CompactMode = other.TUI.CompactMode
277− if other.TUI.CompactMode {
408+ if other.Debug {
278− result.TUI.CompactMode = other.TUI.CompactMode
409+ baseOptions.Debug = other.Debug
281− if other.Debug {
412+ if other.DebugLSP {
282− result.Debug = other.Debug
413+ baseOptions.DebugLSP = other.DebugLSP
285− if other.DebugLSP {
416+ if other.DisableAutoSummarize {
286− result.DebugLSP = other.DebugLSP
417+ baseOptions.DisableAutoSummarize = other.DisableAutoSummarize
420+ if other.DataDirectory != "" {
421+ baseOptions.DataDirectory = other.DataDirectory
423+ base.Options = baseOptions
289− if other.DisableAutoSummarize {
427+func mergeAgents(base, global, local *Config) {
290− result.DisableAutoSummarize = other.DisableAutoSummarize
428+ for _, cfg := range []*Config{global, local} {
432+ for agentID, globalAgent := range cfg.Agents {
433+ if _, ok := base.Agents[agentID]; !ok {
434+ base.Agents[agentID] = globalAgent
438+ baseAgent := base.Agents[agentID]
439+ baseAgent.Model = globalAgent.Model
440+ baseAgent.Provider = globalAgent.Provider
441+ baseAgent.AllowedMCP = globalAgent.AllowedMCP
442+ baseAgent.AllowedLSP = globalAgent.AllowedLSP
443+ base.Agents[agentID] = baseAgent
445+ baseAgent := base.Agents[agentID]
446+ baseAgent.Model = globalAgent.Model
447+ baseAgent.Provider = globalAgent.Provider
448+ base.Agents[agentID] = baseAgent
450+ baseAgent := base.Agents[agentID]
451+ baseAgent.Model = globalAgent.Model
452+ baseAgent.Provider = globalAgent.Provider
453+ base.Agents[agentID] = baseAgent
454+ case AgentSummarize:
455+ baseAgent := base.Agents[agentID]
456+ baseAgent.Model = globalAgent.Model
457+ baseAgent.Provider = globalAgent.Provider
458+ base.Agents[agentID] = baseAgent
460+ baseAgent := base.Agents[agentID]
461+ baseAgent.Name = globalAgent.Name
462+ baseAgent.Description = globalAgent.Description
463+ baseAgent.Disabled = globalAgent.Disabled
464+ baseAgent.Provider = globalAgent.Provider
465+ baseAgent.Model = globalAgent.Model
466+ baseAgent.AllowedTools = globalAgent.AllowedTools
467+ baseAgent.AllowedMCP = globalAgent.AllowedMCP
468+ baseAgent.AllowedLSP = globalAgent.AllowedLSP
469+ base.Agents[agentID] = baseAgent
293− if other.DataDirectory != "" {
477+func mergeMCPs(base, global, local *Config) {
294− result.DataDirectory = other.DataDirectory
478+ for _, cfg := range []*Config{global, local} {
482+ maps.Copy(base.MCP, cfg.MCP)
297− return result
486+func mergeLSPs(base, global, local *Config) {
487+ for _, cfg := range []*Config{global, local} {
491+ maps.Copy(base.LSP, cfg.LSP)
300func mergeProviderConfigs(base, global, local *Config) {
495func mergeProviderConfigs(base, global, local *Config) {
496+ for _, cfg := range []*Config{global, local} {
301− if global != nil {
497+ if cfg == nil {
302− for providerName, globalProvider := range global.Providers {
500+ for providerName, globalProvider := range cfg.Providers {
303 if _, ok := base.Providers[providerName]; !ok {
501 if _, ok := base.Providers[providerName]; !ok {
304 base.Providers[providerName] = globalProvider
502 base.Providers[providerName] = globalProvider
@@ -307,15 +505,6 @@ func mergeProviderConfigs(base, global, local *Config) {
311− for providerName, localProvider := range local.Providers {
312− if _, ok := base.Providers[providerName]; !ok {
313− base.Providers[providerName] = localProvider
315− base.Providers[providerName] = mergeProviderConfig(providerName, base.Providers[providerName], localProvider)
320 finalProviders := make(map[provider.InferenceProvider]ProviderConfig)
509 finalProviders := make(map[provider.InferenceProvider]ProviderConfig)
321 for providerName, providerConfig := range base.Providers {
510 for providerName, providerConfig := range base.Providers {
@@ -328,30 +517,36 @@ func mergeProviderConfigs(base, global, local *Config) {
328 base.Providers = finalProviders
517 base.Providers = finalProviders
331−func providerDefaultConfig(providerName provider.InferenceProvider) ProviderConfig {
520+func providerDefaultConfig(providerId provider.InferenceProvider) ProviderConfig {
332− switch providerName {
521+ switch providerId {
333 case provider.InferenceProviderAnthropic:
522 case provider.InferenceProviderAnthropic:
334 return ProviderConfig{
523 return ProviderConfig{
335 ProviderType: provider.TypeAnthropic,
525 ProviderType: provider.TypeAnthropic,
337 case provider.InferenceProviderOpenAI:
527 case provider.InferenceProviderOpenAI:
338 return ProviderConfig{
528 return ProviderConfig{
339 ProviderType: provider.TypeOpenAI,
530 ProviderType: provider.TypeOpenAI,
341 case provider.InferenceProviderGemini:
532 case provider.InferenceProviderGemini:
342 return ProviderConfig{
533 return ProviderConfig{
343 ProviderType: provider.TypeGemini,
535 ProviderType: provider.TypeGemini,
345 case provider.InferenceProviderBedrock:
537 case provider.InferenceProviderBedrock:
346 return ProviderConfig{
538 return ProviderConfig{
347 ProviderType: provider.TypeBedrock,
540 ProviderType: provider.TypeBedrock,
349 case provider.InferenceProviderAzure:
542 case provider.InferenceProviderAzure:
350 return ProviderConfig{
543 return ProviderConfig{
351 ProviderType: provider.TypeAzure,
545 ProviderType: provider.TypeAzure,
353 case provider.InferenceProviderOpenRouter:
547 case provider.InferenceProviderOpenRouter:
354 return ProviderConfig{
548 return ProviderConfig{
355 ProviderType: provider.TypeOpenAI,
550 ProviderType: provider.TypeOpenAI,
356 BaseURL: "https://openrouter.ai/api/v1",
551 BaseURL: "https://openrouter.ai/api/v1",
357 ExtraHeaders: map[string]string{
552 ExtraHeaders: map[string]string{
@@ -361,15 +556,18 @@ func providerDefaultConfig(providerName provider.InferenceProvider) ProviderConf
362 case provider.InferenceProviderXAI:
557 case provider.InferenceProviderXAI:
363 return ProviderConfig{
558 return ProviderConfig{
364 ProviderType: provider.TypeXAI,
560 ProviderType: provider.TypeXAI,
365 BaseURL: "https://api.x.ai/v1",
561 BaseURL: "https://api.x.ai/v1",
367 case provider.InferenceProviderVertexAI:
563 case provider.InferenceProviderVertexAI:
368 return ProviderConfig{
564 return ProviderConfig{
369 ProviderType: provider.TypeVertexAI,
566 ProviderType: provider.TypeVertexAI,
372 return ProviderConfig{
569 return ProviderConfig{
373 ProviderType: provider.TypeOpenAI,
571 ProviderType: provider.TypeOpenAI,
@@ -379,6 +577,7 @@ func defaultConfigBasedOnEnv() *Config {
379 cfg := &Config{
577 cfg := &Config{
380 Options: Options{
578 Options: Options{
381 DataDirectory: defaultDataDirectory,
579 DataDirectory: defaultDataDirectory,
580+ ContextPaths: defaultContextPaths,
383 Providers: make(map[provider.InferenceProvider]ProviderConfig),
582 Providers: make(map[provider.InferenceProvider]ProviderConfig),
@@ -391,7 +590,22 @@ func defaultConfigBasedOnEnv() *Config {
391 if apiKey := os.Getenv(envVar); apiKey != "" {
590 if apiKey := os.Getenv(envVar); apiKey != "" {
392 providerConfig := providerDefaultConfig(p.ID)
591 providerConfig := providerDefaultConfig(p.ID)
393 providerConfig.APIKey = apiKey
592 providerConfig.APIKey = apiKey
394− providerConfig.DefaultModel = p.DefaultModelID
593+ providerConfig.DefaultLargeModel = p.DefaultLargeModelID
594+ providerConfig.DefaultSmallModel = p.DefaultSmallModelID
595+ for _, model := range p.Models {
596+ providerConfig.Models = append(providerConfig.Models, Model{
599+ CostPer1MIn: model.CostPer1MIn,
600+ CostPer1MOut: model.CostPer1MOut,
601+ CostPer1MInCached: model.CostPer1MInCached,
602+ CostPer1MOutCached: model.CostPer1MOutCached,
603+ ContextWindow: model.ContextWindow,
604+ DefaultMaxTokens: model.DefaultMaxTokens,
605+ CanReason: model.CanReason,
606+ SupportsImages: model.SupportsImages,
395 cfg.Providers[p.ID] = providerConfig
609 cfg.Providers[p.ID] = providerConfig