| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778 |
- // Package config manages application configuration from various sources.
- package config
- import (
- "encoding/json"
- "fmt"
- "log/slog"
- "os"
- "strings"
- "github.com/opencode-ai/opencode/internal/llm/models"
- "github.com/opencode-ai/opencode/internal/logging"
- "github.com/spf13/viper"
- )
- // MCPType defines the type of MCP (Model Control Protocol) server.
- type MCPType string
- // Supported MCP types
- const (
- MCPStdio MCPType = "stdio"
- MCPSse MCPType = "sse"
- )
- // MCPServer defines the configuration for a Model Control Protocol server.
- type MCPServer struct {
- Command string `json:"command"`
- Env []string `json:"env"`
- Args []string `json:"args"`
- Type MCPType `json:"type"`
- URL string `json:"url"`
- Headers map[string]string `json:"headers"`
- }
- type AgentName string
- const (
- AgentCoder AgentName = "coder"
- AgentTask AgentName = "task"
- AgentTitle AgentName = "title"
- )
- // Agent defines configuration for different LLM models and their token limits.
- type Agent struct {
- Model models.ModelID `json:"model"`
- MaxTokens int64 `json:"maxTokens"`
- ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh
- }
- // Provider defines configuration for an LLM provider.
- type Provider struct {
- APIKey string `json:"apiKey"`
- Disabled bool `json:"disabled"`
- }
- // Data defines storage configuration.
- type Data struct {
- Directory string `json:"directory"`
- }
- // LSPConfig defines configuration for Language Server Protocol integration.
- type LSPConfig struct {
- Disabled bool `json:"enabled"`
- Command string `json:"command"`
- Args []string `json:"args"`
- Options any `json:"options"`
- }
- // TUIConfig defines the configuration for the Terminal User Interface.
- type TUIConfig struct {
- Theme string `json:"theme,omitempty"`
- CustomTheme map[string]any `json:"customTheme,omitempty"`
- }
- // Config is the main configuration structure for the application.
- type Config struct {
- Data Data `json:"data"`
- WorkingDir string `json:"wd,omitempty"`
- MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
- Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
- LSP map[string]LSPConfig `json:"lsp,omitempty"`
- Agents map[AgentName]Agent `json:"agents"`
- Debug bool `json:"debug,omitempty"`
- DebugLSP bool `json:"debugLSP,omitempty"`
- ContextPaths []string `json:"contextPaths,omitempty"`
- TUI TUIConfig `json:"tui"`
- }
- // Application constants
- const (
- defaultDataDirectory = ".opencode"
- defaultLogLevel = "info"
- appName = "opencode"
- MaxTokensFallbackDefault = 4096
- )
- var defaultContextPaths = []string{
- ".github/copilot-instructions.md",
- ".cursorrules",
- ".cursor/rules/",
- "CLAUDE.md",
- "CLAUDE.local.md",
- "opencode.md",
- "opencode.local.md",
- "OpenCode.md",
- "OpenCode.local.md",
- "OPENCODE.md",
- "OPENCODE.local.md",
- }
- // Global configuration instance
- var cfg *Config
- // Load initializes the configuration from environment variables and config files.
- // If debug is true, debug mode is enabled and log level is set to debug.
- // It returns an error if configuration loading fails.
- func Load(workingDir string, debug bool) (*Config, error) {
- if cfg != nil {
- return cfg, nil
- }
- cfg = &Config{
- WorkingDir: workingDir,
- MCPServers: make(map[string]MCPServer),
- Providers: make(map[models.ModelProvider]Provider),
- LSP: make(map[string]LSPConfig),
- }
- configureViper()
- setDefaults(debug)
- setProviderDefaults()
- // Read global config
- if err := readConfig(viper.ReadInConfig()); err != nil {
- return cfg, err
- }
- // Load and merge local config
- mergeLocalConfig(workingDir)
- // Apply configuration to the struct
- if err := viper.Unmarshal(cfg); err != nil {
- return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
- }
- applyDefaultValues()
- defaultLevel := slog.LevelInfo
- if cfg.Debug {
- defaultLevel = slog.LevelDebug
- }
- if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
- loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
- // if file does not exist create it
- if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
- if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
- return cfg, fmt.Errorf("failed to create directory: %w", err)
- }
- if _, err := os.Create(loggingFile); err != nil {
- return cfg, fmt.Errorf("failed to create log file: %w", err)
- }
- }
- sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
- if err != nil {
- return cfg, fmt.Errorf("failed to open log file: %w", err)
- }
- // Configure logger
- logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
- Level: defaultLevel,
- }))
- slog.SetDefault(logger)
- } else {
- // Configure logger
- logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
- Level: defaultLevel,
- }))
- slog.SetDefault(logger)
- }
- // Validate configuration
- if err := Validate(); err != nil {
- return cfg, fmt.Errorf("config validation failed: %w", err)
- }
- if cfg.Agents == nil {
- cfg.Agents = make(map[AgentName]Agent)
- }
- // Override the max tokens for title agent
- cfg.Agents[AgentTitle] = Agent{
- Model: cfg.Agents[AgentTitle].Model,
- MaxTokens: 80,
- }
- return cfg, nil
- }
- // configureViper sets up viper's configuration paths and environment variables.
- func configureViper() {
- viper.SetConfigName(fmt.Sprintf(".%s", appName))
- viper.SetConfigType("json")
- viper.AddConfigPath("$HOME")
- viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
- viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
- viper.SetEnvPrefix(strings.ToUpper(appName))
- viper.AutomaticEnv()
- }
- // setDefaults configures default values for configuration options.
- func setDefaults(debug bool) {
- viper.SetDefault("data.directory", defaultDataDirectory)
- viper.SetDefault("contextPaths", defaultContextPaths)
- viper.SetDefault("tui.theme", "opencode")
- if debug {
- viper.SetDefault("debug", true)
- viper.Set("log.level", "debug")
- } else {
- viper.SetDefault("debug", false)
- viper.SetDefault("log.level", defaultLogLevel)
- }
- }
- // setProviderDefaults configures LLM provider defaults based on environment variables.
- func setProviderDefaults() {
- // Set all API keys we can find in the environment
- if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
- viper.SetDefault("providers.anthropic.apiKey", apiKey)
- }
- if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
- viper.SetDefault("providers.openai.apiKey", apiKey)
- }
- if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
- viper.SetDefault("providers.gemini.apiKey", apiKey)
- }
- if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
- viper.SetDefault("providers.groq.apiKey", apiKey)
- }
- if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
- viper.SetDefault("providers.openrouter.apiKey", apiKey)
- }
- // Use this order to set the default models
- // 1. Anthropic
- // 2. OpenAI
- // 3. Google Gemini
- // 4. Groq
- // 5. AWS Bedrock
- // Anthropic configuration
- if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
- viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
- viper.SetDefault("agents.task.model", models.Claude37Sonnet)
- viper.SetDefault("agents.title.model", models.Claude37Sonnet)
- return
- }
- // OpenAI configuration
- if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
- viper.SetDefault("agents.coder.model", models.GPT41)
- viper.SetDefault("agents.task.model", models.GPT41Mini)
- viper.SetDefault("agents.title.model", models.GPT41Mini)
- return
- }
- // Google Gemini configuration
- if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
- viper.SetDefault("agents.coder.model", models.Gemini25)
- viper.SetDefault("agents.task.model", models.Gemini25Flash)
- viper.SetDefault("agents.title.model", models.Gemini25Flash)
- return
- }
- // Groq configuration
- if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
- viper.SetDefault("agents.coder.model", models.QWENQwq)
- viper.SetDefault("agents.task.model", models.QWENQwq)
- viper.SetDefault("agents.title.model", models.QWENQwq)
- return
- }
- // OpenRouter configuration
- if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
- viper.SetDefault("providers.openrouter.apiKey", apiKey)
- viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet)
- viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet)
- viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku)
- return
- }
- // AWS Bedrock configuration
- if hasAWSCredentials() {
- viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
- viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
- viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
- return
- }
- if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" {
- // api-key may be empty when using Entra ID credentials – that's okay
- viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
- viper.SetDefault("agents.coder.model", models.AzureGPT41)
- viper.SetDefault("agents.task.model", models.AzureGPT41Mini)
- viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
- return
- }
- }
- // hasAWSCredentials checks if AWS credentials are available in the environment.
- func hasAWSCredentials() bool {
- // Check for explicit AWS credentials
- if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
- return true
- }
- // Check for AWS profile
- if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
- return true
- }
- // Check for AWS region
- if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
- return true
- }
- // Check if running on EC2 with instance profile
- if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
- os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
- return true
- }
- return false
- }
- // readConfig handles the result of reading a configuration file.
- func readConfig(err error) error {
- if err == nil {
- return nil
- }
- // It's okay if the config file doesn't exist
- if _, ok := err.(viper.ConfigFileNotFoundError); ok {
- return nil
- }
- return fmt.Errorf("failed to read config: %w", err)
- }
- // mergeLocalConfig loads and merges configuration from the local directory.
- func mergeLocalConfig(workingDir string) {
- local := viper.New()
- local.SetConfigName(fmt.Sprintf(".%s", appName))
- local.SetConfigType("json")
- local.AddConfigPath(workingDir)
- // Merge local config if it exists
- if err := local.ReadInConfig(); err == nil {
- viper.MergeConfigMap(local.AllSettings())
- }
- }
- // applyDefaultValues sets default values for configuration fields that need processing.
- func applyDefaultValues() {
- // Set default MCP type if not specified
- for k, v := range cfg.MCPServers {
- if v.Type == "" {
- v.Type = MCPStdio
- cfg.MCPServers[k] = v
- }
- }
- }
- // It validates model IDs and providers, ensuring they are supported.
- func validateAgent(cfg *Config, name AgentName, agent Agent) error {
- // Check if model exists
- model, modelExists := models.SupportedModels[agent.Model]
- if !modelExists {
- logging.Warn("unsupported model configured, reverting to default",
- "agent", name,
- "configured_model", agent.Model)
- // Set default model based on available providers
- if setDefaultModelForAgent(name) {
- logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
- } else {
- return fmt.Errorf("no valid provider available for agent %s", name)
- }
- return nil
- }
- // Check if provider for the model is configured
- provider := model.Provider
- providerCfg, providerExists := cfg.Providers[provider]
- if !providerExists {
- // Provider not configured, check if we have environment variables
- apiKey := getProviderAPIKey(provider)
- if apiKey == "" {
- logging.Warn("provider not configured for model, reverting to default",
- "agent", name,
- "model", agent.Model,
- "provider", provider)
- // Set default model based on available providers
- if setDefaultModelForAgent(name) {
- logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
- } else {
- return fmt.Errorf("no valid provider available for agent %s", name)
- }
- } else {
- // Add provider with API key from environment
- cfg.Providers[provider] = Provider{
- APIKey: apiKey,
- }
- logging.Info("added provider from environment", "provider", provider)
- }
- } else if providerCfg.Disabled || providerCfg.APIKey == "" {
- // Provider is disabled or has no API key
- logging.Warn("provider is disabled or has no API key, reverting to default",
- "agent", name,
- "model", agent.Model,
- "provider", provider)
- // Set default model based on available providers
- if setDefaultModelForAgent(name) {
- logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
- } else {
- return fmt.Errorf("no valid provider available for agent %s", name)
- }
- }
- // Validate max tokens
- if agent.MaxTokens <= 0 {
- logging.Warn("invalid max tokens, setting to default",
- "agent", name,
- "model", agent.Model,
- "max_tokens", agent.MaxTokens)
- // Update the agent with default max tokens
- updatedAgent := cfg.Agents[name]
- if model.DefaultMaxTokens > 0 {
- updatedAgent.MaxTokens = model.DefaultMaxTokens
- } else {
- updatedAgent.MaxTokens = MaxTokensFallbackDefault
- }
- cfg.Agents[name] = updatedAgent
- } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
- // Ensure max tokens doesn't exceed half the context window (reasonable limit)
- logging.Warn("max tokens exceeds half the context window, adjusting",
- "agent", name,
- "model", agent.Model,
- "max_tokens", agent.MaxTokens,
- "context_window", model.ContextWindow)
- // Update the agent with adjusted max tokens
- updatedAgent := cfg.Agents[name]
- updatedAgent.MaxTokens = model.ContextWindow / 2
- cfg.Agents[name] = updatedAgent
- }
- // Validate reasoning effort for models that support reasoning
- if model.CanReason && provider == models.ProviderOpenAI {
- if agent.ReasoningEffort == "" {
- // Set default reasoning effort for models that support it
- logging.Info("setting default reasoning effort for model that supports reasoning",
- "agent", name,
- "model", agent.Model)
- // Update the agent with default reasoning effort
- updatedAgent := cfg.Agents[name]
- updatedAgent.ReasoningEffort = "medium"
- cfg.Agents[name] = updatedAgent
- } else {
- // Check if reasoning effort is valid (low, medium, high)
- effort := strings.ToLower(agent.ReasoningEffort)
- if effort != "low" && effort != "medium" && effort != "high" {
- logging.Warn("invalid reasoning effort, setting to medium",
- "agent", name,
- "model", agent.Model,
- "reasoning_effort", agent.ReasoningEffort)
- // Update the agent with valid reasoning effort
- updatedAgent := cfg.Agents[name]
- updatedAgent.ReasoningEffort = "medium"
- cfg.Agents[name] = updatedAgent
- }
- }
- } else if !model.CanReason && agent.ReasoningEffort != "" {
- // Model doesn't support reasoning but reasoning effort is set
- logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
- "agent", name,
- "model", agent.Model,
- "reasoning_effort", agent.ReasoningEffort)
- // Update the agent to remove reasoning effort
- updatedAgent := cfg.Agents[name]
- updatedAgent.ReasoningEffort = ""
- cfg.Agents[name] = updatedAgent
- }
- return nil
- }
- // Validate checks if the configuration is valid and applies defaults where needed.
- func Validate() error {
- if cfg == nil {
- return fmt.Errorf("config not loaded")
- }
- // Validate agent models
- for name, agent := range cfg.Agents {
- if err := validateAgent(cfg, name, agent); err != nil {
- return err
- }
- }
- // Validate providers
- for provider, providerCfg := range cfg.Providers {
- if providerCfg.APIKey == "" && !providerCfg.Disabled {
- logging.Warn("provider has no API key, marking as disabled", "provider", provider)
- providerCfg.Disabled = true
- cfg.Providers[provider] = providerCfg
- }
- }
- // Validate LSP configurations
- for language, lspConfig := range cfg.LSP {
- if lspConfig.Command == "" && !lspConfig.Disabled {
- logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
- lspConfig.Disabled = true
- cfg.LSP[language] = lspConfig
- }
- }
- return nil
- }
- // getProviderAPIKey gets the API key for a provider from environment variables
- func getProviderAPIKey(provider models.ModelProvider) string {
- switch provider {
- case models.ProviderAnthropic:
- return os.Getenv("ANTHROPIC_API_KEY")
- case models.ProviderOpenAI:
- return os.Getenv("OPENAI_API_KEY")
- case models.ProviderGemini:
- return os.Getenv("GEMINI_API_KEY")
- case models.ProviderGROQ:
- return os.Getenv("GROQ_API_KEY")
- case models.ProviderAzure:
- return os.Getenv("AZURE_OPENAI_API_KEY")
- case models.ProviderOpenRouter:
- return os.Getenv("OPENROUTER_API_KEY")
- case models.ProviderBedrock:
- if hasAWSCredentials() {
- return "aws-credentials-available"
- }
- }
- return ""
- }
- // setDefaultModelForAgent sets a default model for an agent based on available providers
- func setDefaultModelForAgent(agent AgentName) bool {
- // Check providers in order of preference
- if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
- maxTokens := int64(5000)
- if agent == AgentTitle {
- maxTokens = 80
- }
- cfg.Agents[agent] = Agent{
- Model: models.Claude37Sonnet,
- MaxTokens: maxTokens,
- }
- return true
- }
- if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
- var model models.ModelID
- maxTokens := int64(5000)
- reasoningEffort := ""
- switch agent {
- case AgentTitle:
- model = models.GPT41Mini
- maxTokens = 80
- case AgentTask:
- model = models.GPT41Mini
- default:
- model = models.GPT41
- }
- // Check if model supports reasoning
- if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
- reasoningEffort = "medium"
- }
- cfg.Agents[agent] = Agent{
- Model: model,
- MaxTokens: maxTokens,
- ReasoningEffort: reasoningEffort,
- }
- return true
- }
- if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
- var model models.ModelID
- maxTokens := int64(5000)
- reasoningEffort := ""
- switch agent {
- case AgentTitle:
- model = models.OpenRouterClaude35Haiku
- maxTokens = 80
- case AgentTask:
- model = models.OpenRouterClaude37Sonnet
- default:
- model = models.OpenRouterClaude37Sonnet
- }
- // Check if model supports reasoning
- if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
- reasoningEffort = "medium"
- }
- cfg.Agents[agent] = Agent{
- Model: model,
- MaxTokens: maxTokens,
- ReasoningEffort: reasoningEffort,
- }
- return true
- }
- if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
- var model models.ModelID
- maxTokens := int64(5000)
- if agent == AgentTitle {
- model = models.Gemini25Flash
- maxTokens = 80
- } else {
- model = models.Gemini25
- }
- cfg.Agents[agent] = Agent{
- Model: model,
- MaxTokens: maxTokens,
- }
- return true
- }
- if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
- maxTokens := int64(5000)
- if agent == AgentTitle {
- maxTokens = 80
- }
- cfg.Agents[agent] = Agent{
- Model: models.QWENQwq,
- MaxTokens: maxTokens,
- }
- return true
- }
- if hasAWSCredentials() {
- maxTokens := int64(5000)
- if agent == AgentTitle {
- maxTokens = 80
- }
- cfg.Agents[agent] = Agent{
- Model: models.BedrockClaude37Sonnet,
- MaxTokens: maxTokens,
- ReasoningEffort: "medium", // Claude models support reasoning
- }
- return true
- }
- return false
- }
- // Get returns the current configuration.
- // It's safe to call this function multiple times.
- func Get() *Config {
- return cfg
- }
- // WorkingDirectory returns the current working directory from the configuration.
- func WorkingDirectory() string {
- if cfg == nil {
- panic("config not loaded")
- }
- return cfg.WorkingDir
- }
- func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
- if cfg == nil {
- panic("config not loaded")
- }
- existingAgentCfg := cfg.Agents[agentName]
- model, ok := models.SupportedModels[modelID]
- if !ok {
- return fmt.Errorf("model %s not supported", modelID)
- }
- maxTokens := existingAgentCfg.MaxTokens
- if model.DefaultMaxTokens > 0 {
- maxTokens = model.DefaultMaxTokens
- }
- newAgentCfg := Agent{
- Model: modelID,
- MaxTokens: maxTokens,
- ReasoningEffort: existingAgentCfg.ReasoningEffort,
- }
- cfg.Agents[agentName] = newAgentCfg
- if err := validateAgent(cfg, agentName, newAgentCfg); err != nil {
- // revert config update on failure
- cfg.Agents[agentName] = existingAgentCfg
- return fmt.Errorf("failed to update agent model: %w", err)
- }
- return nil
- }
- // UpdateTheme updates the theme in the configuration and writes it to the config file.
- func UpdateTheme(themeName string) error {
- if cfg == nil {
- return fmt.Errorf("config not loaded")
- }
- // Update the in-memory config
- cfg.TUI.Theme = themeName
- // Get the config file path
- configFile := viper.ConfigFileUsed()
- if configFile == "" {
- // If no config file exists yet, create one with default settings
- viper.Set("tui.theme", themeName)
- return viper.SafeWriteConfig()
- }
- // Read the existing config file
- configData, err := os.ReadFile(configFile)
- if err != nil {
- return fmt.Errorf("failed to read config file: %w", err)
- }
- // Parse the JSON
- var configMap map[string]any
- if err := json.Unmarshal(configData, &configMap); err != nil {
- return fmt.Errorf("failed to parse config file: %w", err)
- }
- // Update just the theme value
- tuiConfig, ok := configMap["tui"].(map[string]any)
- if !ok {
- // TUI config doesn't exist yet, create it
- configMap["tui"] = map[string]any{"theme": themeName}
- } else {
- // Update existing TUI config
- tuiConfig["theme"] = themeName
- configMap["tui"] = tuiConfig
- }
- // Write the updated config back to file
- updatedData, err := json.MarshalIndent(configMap, "", " ")
- if err != nil {
- return fmt.Errorf("failed to marshal config: %w", err)
- }
- if err := os.WriteFile(configFile, updatedData, 0644); err != nil {
- return fmt.Errorf("failed to write config file: %w", err)
- }
- return nil
- }
|