فهرست منبع

feat: custom themes

adamdottv 9 ماه پیش
والد
کامیت
91ae9b33d3
7فایلهای تغییر یافته به همراه374 افزوده شده و 98 حذف شده
  1. 71 2
      README.md
  2. 28 0
      cmd/schema/main.go
  3. 5 4
      internal/config/config.go
  4. 0 1
      internal/tui/styles/icons.go
  5. 100 1
      internal/tui/theme/manager.go
  6. 50 1
      internal/tui/theme/theme.go
  7. 120 89
      opencode-schema.json

+ 71 - 2
README.md

@@ -67,7 +67,7 @@ OpenCode looks for configuration in the following locations:
 You can configure OpenCode using environment variables:
 You can configure OpenCode using environment variables:
 
 
 | Environment Variable       | Purpose                                                |
 | Environment Variable       | Purpose                                                |
-|----------------------------|--------------------------------------------------------|
+| -------------------------- | ------------------------------------------------------ |
 | `ANTHROPIC_API_KEY`        | For Claude models                                      |
 | `ANTHROPIC_API_KEY`        | For Claude models                                      |
 | `OPENAI_API_KEY`           | For OpenAI models                                      |
 | `OPENAI_API_KEY`           | For OpenAI models                                      |
 | `GEMINI_API_KEY`           | For Google Gemini models                               |
 | `GEMINI_API_KEY`           | For Google Gemini models                               |
@@ -79,7 +79,6 @@ You can configure OpenCode using environment variables:
 | `AZURE_OPENAI_API_KEY`     | For Azure OpenAI models (optional when using Entra ID) |
 | `AZURE_OPENAI_API_KEY`     | For Azure OpenAI models (optional when using Entra ID) |
 | `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models                                |
 | `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models                                |
 
 
-
 ### Configuration File Structure
 ### Configuration File Structure
 
 
 ```json
 ```json
@@ -303,6 +302,76 @@ OpenCode's AI assistant has access to various tools to help with coding tasks:
 | `sourcegraph` | Search code across public repositories | `query` (required), `count` (optional), `context_window` (optional), `timeout` (optional) |
 | `sourcegraph` | Search code across public repositories | `query` (required), `count` (optional), `context_window` (optional), `timeout` (optional) |
 | `agent`       | Run sub-tasks with the AI agent        | `prompt` (required)                                                                       |
 | `agent`       | Run sub-tasks with the AI agent        | `prompt` (required)                                                                       |
 
 
+## Theming
+
+OpenCode supports multiple themes for customizing the appearance of the terminal interface.
+
+### Available Themes
+
+The following predefined themes are available:
+
+- `opencode` (default)
+- `catppuccin`
+- `dracula`
+- `flexoki`
+- `gruvbox`
+- `monokai`
+- `onedark`
+- `tokyonight`
+- `tron`
+- `custom` (user-defined)
+
+### Setting a Theme
+
+You can set a theme in your `.opencode.json` configuration file:
+
+```json
+{
+  "tui": {
+    "theme": "monokai"
+  }
+}
+```
+
+### Custom Themes
+
+You can define your own custom theme by setting the `theme` to `"custom"` and providing color definitions in the `customTheme` map:
+
+```json
+{
+  "tui": {
+    "theme": "custom",
+    "customTheme": {
+      "primary": "#ffcc00",
+      "secondary": "#00ccff",
+      "accent": { "dark": "#aa00ff", "light": "#ddccff" },
+      "error": "#ff0000"
+    }
+  }
+}
+```
+
+#### Color Definition Formats
+
+Custom theme colors support two formats:
+
+1. **Simple Hex String**: A single hex color string (e.g., `"#aabbcc"`) that will be used for both light and dark terminal backgrounds.
+
+2. **Adaptive Object**: An object with `dark` and `light` keys, each holding a hex color string. This allows for adaptive colors based on the terminal's background.
+
+#### Available Color Keys
+
+You can define any of the following color keys in your `customTheme`:
+
+- Base colors: `primary`, `secondary`, `accent`
+- Status colors: `error`, `warning`, `success`, `info`
+- Text colors: `text`, `textMuted`, `textEmphasized`
+- Background colors: `background`, `backgroundSecondary`, `backgroundDarker`
+- Border colors: `borderNormal`, `borderFocused`, `borderDim`
+- Diff view colors: `diffAdded`, `diffRemoved`, `diffContext`, etc.
+
+You don't need to define all colors. Any undefined colors will fall back to the default "opencode" theme colors.
+
 ## Architecture
 ## Architecture
 
 
 OpenCode is built with a modular architecture:
 OpenCode is built with a modular architecture:

+ 28 - 0
cmd/schema/main.go

@@ -116,6 +116,34 @@ func generateSchema() map[string]any {
 					"onedark",
 					"onedark",
 					"tokyonight",
 					"tokyonight",
 					"tron",
 					"tron",
+					"custom",
+				},
+			},
+			"customTheme": map[string]any{
+				"type":        "object",
+				"description": "Custom theme color definitions",
+				"additionalProperties": map[string]any{
+					"oneOf": []map[string]any{
+						{
+							"type":    "string",
+							"pattern": "^#[0-9a-fA-F]{6}$",
+						},
+						{
+							"type": "object",
+							"properties": map[string]any{
+								"dark": map[string]any{
+									"type":    "string",
+									"pattern": "^#[0-9a-fA-F]{6}$",
+								},
+								"light": map[string]any{
+									"type":    "string",
+									"pattern": "^#[0-9a-fA-F]{6}$",
+								},
+							},
+							"required":             []string{"dark", "light"},
+							"additionalProperties": false,
+						},
+					},
 				},
 				},
 			},
 			},
 		},
 		},

+ 5 - 4
internal/config/config.go

@@ -68,7 +68,8 @@ type LSPConfig struct {
 
 
 // TUIConfig defines the configuration for the Terminal User Interface.
 // TUIConfig defines the configuration for the Terminal User Interface.
 type TUIConfig struct {
 type TUIConfig struct {
-	Theme string `json:"theme,omitempty"`
+	Theme       string                 `json:"theme,omitempty"`
+	CustomTheme map[string]any `json:"customTheme,omitempty"`
 }
 }
 
 
 // Config is the main configuration structure for the application.
 // Config is the main configuration structure for the application.
@@ -747,16 +748,16 @@ func UpdateTheme(themeName string) error {
 	}
 	}
 
 
 	// Parse the JSON
 	// Parse the JSON
-	var configMap map[string]interface{}
+	var configMap map[string]any
 	if err := json.Unmarshal(configData, &configMap); err != nil {
 	if err := json.Unmarshal(configData, &configMap); err != nil {
 		return fmt.Errorf("failed to parse config file: %w", err)
 		return fmt.Errorf("failed to parse config file: %w", err)
 	}
 	}
 
 
 	// Update just the theme value
 	// Update just the theme value
-	tuiConfig, ok := configMap["tui"].(map[string]interface{})
+	tuiConfig, ok := configMap["tui"].(map[string]any)
 	if !ok {
 	if !ok {
 		// TUI config doesn't exist yet, create it
 		// TUI config doesn't exist yet, create it
-		configMap["tui"] = map[string]interface{}{"theme": themeName}
+		configMap["tui"] = map[string]any{"theme": themeName}
 	} else {
 	} else {
 		// Update existing TUI config
 		// Update existing TUI config
 		tuiConfig["theme"] = themeName
 		tuiConfig["theme"] = themeName

+ 0 - 1
internal/tui/styles/icons.go

@@ -11,4 +11,3 @@ const (
 	SpinnerIcon string = "..."
 	SpinnerIcon string = "..."
 	LoadingIcon string = "⟳"
 	LoadingIcon string = "⟳"
 )
 )
-

+ 100 - 1
internal/tui/theme/manager.go

@@ -25,6 +25,9 @@ var globalManager = &Manager{
 	currentName: "",
 	currentName: "",
 }
 }
 
 
+// Default theme instance for custom theme defaulting
+var defaultThemeColors = NewOpenCodeTheme()
+
 // RegisterTheme adds a new theme to the registry.
 // RegisterTheme adds a new theme to the registry.
 // If this is the first theme registered, it becomes the default.
 // If this is the first theme registered, it becomes the default.
 func RegisterTheme(name string, theme Theme) {
 func RegisterTheme(name string, theme Theme) {
@@ -46,7 +49,22 @@ func SetTheme(name string) error {
 	defer globalManager.mu.Unlock()
 	defer globalManager.mu.Unlock()
 
 
 	delete(styles.Registry, "charm")
 	delete(styles.Registry, "charm")
-	if _, exists := globalManager.themes[name]; !exists {
+	
+	// Handle custom theme
+	if name == "custom" {
+		cfg := config.Get()
+		if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
+			return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
+		}
+		
+		customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
+		if err != nil {
+			return fmt.Errorf("failed to load custom theme: %w", err)
+		}
+		
+		// Register the custom theme
+		globalManager.themes["custom"] = customTheme
+	} else if _, exists := globalManager.themes[name]; !exists {
 		return fmt.Errorf("theme '%s' not found", name)
 		return fmt.Errorf("theme '%s' not found", name)
 	}
 	}
 
 
@@ -111,6 +129,87 @@ func GetTheme(name string) Theme {
 	return globalManager.themes[name]
 	return globalManager.themes[name]
 }
 }
 
 
+// LoadCustomTheme creates a new theme instance based on the custom theme colors
+// defined in the configuration. It uses the default OpenCode theme as a base
+// and overrides colors that are specified in the customTheme map.
+func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
+	// Create a new theme based on the default OpenCode theme
+	theme := NewOpenCodeTheme()
+
+	// Process each color in the custom theme map
+	for key, value := range customTheme {
+		adaptiveColor, err := ParseAdaptiveColor(value)
+		if err != nil {
+			logging.Warn("Invalid color definition in custom theme", "key", key, "error", err)
+			continue // Skip this color but continue processing others
+		}
+
+		// Set the color in the theme based on the key
+		switch strings.ToLower(key) {
+		case "primary":
+			theme.PrimaryColor = adaptiveColor
+		case "secondary":
+			theme.SecondaryColor = adaptiveColor
+		case "accent":
+			theme.AccentColor = adaptiveColor
+		case "error":
+			theme.ErrorColor = adaptiveColor
+		case "warning":
+			theme.WarningColor = adaptiveColor
+		case "success":
+			theme.SuccessColor = adaptiveColor
+		case "info":
+			theme.InfoColor = adaptiveColor
+		case "text":
+			theme.TextColor = adaptiveColor
+		case "textmuted":
+			theme.TextMutedColor = adaptiveColor
+		case "textemphasized":
+			theme.TextEmphasizedColor = adaptiveColor
+		case "background":
+			theme.BackgroundColor = adaptiveColor
+		case "backgroundsecondary":
+			theme.BackgroundSecondaryColor = adaptiveColor
+		case "backgrounddarker":
+			theme.BackgroundDarkerColor = adaptiveColor
+		case "bordernormal":
+			theme.BorderNormalColor = adaptiveColor
+		case "borderfocused":
+			theme.BorderFocusedColor = adaptiveColor
+		case "borderdim":
+			theme.BorderDimColor = adaptiveColor
+		case "diffadded":
+			theme.DiffAddedColor = adaptiveColor
+		case "diffremoved":
+			theme.DiffRemovedColor = adaptiveColor
+		case "diffcontext":
+			theme.DiffContextColor = adaptiveColor
+		case "diffhunkheader":
+			theme.DiffHunkHeaderColor = adaptiveColor
+		case "diffhighlightadded":
+			theme.DiffHighlightAddedColor = adaptiveColor
+		case "diffhighlightremoved":
+			theme.DiffHighlightRemovedColor = adaptiveColor
+		case "diffaddedbg":
+			theme.DiffAddedBgColor = adaptiveColor
+		case "diffremovedbg":
+			theme.DiffRemovedBgColor = adaptiveColor
+		case "diffcontextbg":
+			theme.DiffContextBgColor = adaptiveColor
+		case "difflinenumber":
+			theme.DiffLineNumberColor = adaptiveColor
+		case "diffaddedlinenumberbg":
+			theme.DiffAddedLineNumberBgColor = adaptiveColor
+		case "diffremovedlinenumberbg":
+			theme.DiffRemovedLineNumberBgColor = adaptiveColor
+		default:
+			logging.Warn("Unknown color key in custom theme", "key", key)
+		}
+	}
+
+	return theme, nil
+}
+
 // updateConfigTheme updates the theme setting in the configuration file
 // updateConfigTheme updates the theme setting in the configuration file
 func updateConfigTheme(themeName string) error {
 func updateConfigTheme(themeName string) error {
 	// Use the config package to update the theme
 	// Use the config package to update the theme

+ 50 - 1
internal/tui/theme/theme.go

@@ -1,6 +1,9 @@
 package theme
 package theme
 
 
 import (
 import (
+	"fmt"
+	"regexp"
+
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/lipgloss"
 )
 )
 
 
@@ -205,4 +208,50 @@ func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStrin
 func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor }
 func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor }
 func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor }
 func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor }
 func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor }
 func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor }
-func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor }
+func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor }
+
+// ParseAdaptiveColor parses a color value from the config file into a lipgloss.AdaptiveColor.
+// It accepts either a string (hex color) or a map with "dark" and "light" keys.
+func ParseAdaptiveColor(value any) (lipgloss.AdaptiveColor, error) {
+	// Regular expression to validate hex color format
+	hexColorRegex := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
+
+	// Case 1: String value (same color for both dark and light modes)
+	if hexColor, ok := value.(string); ok {
+		if !hexColorRegex.MatchString(hexColor) {
+			return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor)
+		}
+		return lipgloss.AdaptiveColor{
+			Dark:  hexColor,
+			Light: hexColor,
+		}, nil
+	}
+
+	// Case 2: Map with dark and light keys
+	if colorMap, ok := value.(map[string]any); ok {
+		darkVal, darkOk := colorMap["dark"]
+		lightVal, lightOk := colorMap["light"]
+
+		if !darkOk || !lightOk {
+			return lipgloss.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys")
+		}
+
+		darkHex, darkIsString := darkVal.(string)
+		lightHex, lightIsString := lightVal.(string)
+
+		if !darkIsString || !lightIsString {
+			return lipgloss.AdaptiveColor{}, fmt.Errorf("color values must be strings")
+		}
+
+		if !hexColorRegex.MatchString(darkHex) || !hexColorRegex.MatchString(lightHex) {
+			return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid hex color format")
+		}
+
+		return lipgloss.AdaptiveColor{
+			Dark:  darkHex,
+			Light: lightHex,
+		}, nil
+	}
+
+	return lipgloss.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys")
+}

+ 120 - 89
opencode-schema.json

@@ -12,63 +12,63 @@
         "model": {
         "model": {
           "description": "Model ID for the agent",
           "description": "Model ID for the agent",
           "enum": [
           "enum": [
-            "gpt-4o-mini",
-            "o1-pro",
-            "azure.gpt-4o-mini",
-            "openrouter.gpt-4.1-mini",
-            "openrouter.o1-mini",
-            "bedrock.claude-3.7-sonnet",
-            "meta-llama/llama-4-scout-17b-16e-instruct",
-            "openrouter.gpt-4o-mini",
-            "gemini-2.0-flash",
-            "deepseek-r1-distill-llama-70b",
-            "openrouter.claude-3.7-sonnet",
-            "openrouter.gpt-4.5-preview",
-            "azure.o3-mini",
-            "openrouter.claude-3.5-haiku",
             "azure.o1-mini",
             "azure.o1-mini",
-            "openrouter.o1",
-            "openrouter.gemini-2.5",
+            "openrouter.gemini-2.5-flash",
+            "claude-3-haiku",
+            "o1-mini",
+            "qwen-qwq",
             "llama-3.3-70b-versatile",
             "llama-3.3-70b-versatile",
-            "gpt-4.5-preview",
-            "openrouter.claude-3-opus",
             "openrouter.claude-3.5-sonnet",
             "openrouter.claude-3.5-sonnet",
+            "o3-mini",
             "o4-mini",
             "o4-mini",
-            "gemini-2.0-flash-lite",
-            "azure.gpt-4.5-preview",
+            "gpt-4.1",
+            "azure.o3-mini",
+            "openrouter.gpt-4.1-nano",
             "openrouter.gpt-4o",
             "openrouter.gpt-4o",
-            "o1",
+            "gemini-2.5",
             "azure.gpt-4o",
             "azure.gpt-4o",
-            "openrouter.gpt-4.1-nano",
+            "azure.gpt-4o-mini",
+            "claude-3.7-sonnet",
+            "azure.gpt-4.1-nano",
+            "openrouter.o1",
+            "openrouter.claude-3-haiku",
+            "bedrock.claude-3.7-sonnet",
+            "gemini-2.5-flash",
+            "azure.o3",
+            "openrouter.gemini-2.5",
+            "openrouter.o3",
+            "openrouter.o3-mini",
+            "openrouter.gpt-4.1-mini",
+            "openrouter.gpt-4.5-preview",
+            "openrouter.gpt-4o-mini",
+            "gpt-4.1-mini",
+            "meta-llama/llama-4-scout-17b-16e-instruct",
+            "openrouter.o1-mini",
+            "gpt-4.5-preview",
             "o3",
             "o3",
-            "gpt-4.1",
-            "azure.o1",
-            "claude-3-haiku",
+            "openrouter.claude-3.5-haiku",
             "claude-3-opus",
             "claude-3-opus",
-            "gpt-4.1-mini",
-            "openrouter.o4-mini",
-            "openrouter.gemini-2.5-flash",
-            "claude-3.5-haiku",
-            "o3-mini",
-            "azure.o3",
-            "gpt-4o",
-            "azure.gpt-4.1",
-            "openrouter.claude-3-haiku",
-            "gpt-4.1-nano",
-            "azure.gpt-4.1-nano",
-            "claude-3.7-sonnet",
-            "gemini-2.5",
+            "o1-pro",
+            "gemini-2.0-flash",
             "azure.o4-mini",
             "azure.o4-mini",
-            "o1-mini",
-            "qwen-qwq",
+            "openrouter.o4-mini",
+            "claude-3.5-sonnet",
             "meta-llama/llama-4-maverick-17b-128e-instruct",
             "meta-llama/llama-4-maverick-17b-128e-instruct",
+            "azure.o1",
             "openrouter.gpt-4.1",
             "openrouter.gpt-4.1",
             "openrouter.o1-pro",
             "openrouter.o1-pro",
-            "openrouter.o3",
-            "claude-3.5-sonnet",
-            "gemini-2.5-flash",
+            "gpt-4.1-nano",
+            "azure.gpt-4.5-preview",
+            "openrouter.claude-3-opus",
+            "gpt-4o-mini",
+            "o1",
+            "deepseek-r1-distill-llama-70b",
+            "azure.gpt-4.1",
+            "gpt-4o",
             "azure.gpt-4.1-mini",
             "azure.gpt-4.1-mini",
-            "openrouter.o3-mini"
+            "openrouter.claude-3.7-sonnet",
+            "claude-3.5-haiku",
+            "gemini-2.0-flash-lite"
           ],
           ],
           "type": "string"
           "type": "string"
         },
         },
@@ -102,63 +102,63 @@
           "model": {
           "model": {
             "description": "Model ID for the agent",
             "description": "Model ID for the agent",
             "enum": [
             "enum": [
-              "gpt-4o-mini",
-              "o1-pro",
-              "azure.gpt-4o-mini",
-              "openrouter.gpt-4.1-mini",
-              "openrouter.o1-mini",
-              "bedrock.claude-3.7-sonnet",
-              "meta-llama/llama-4-scout-17b-16e-instruct",
-              "openrouter.gpt-4o-mini",
-              "gemini-2.0-flash",
-              "deepseek-r1-distill-llama-70b",
-              "openrouter.claude-3.7-sonnet",
-              "openrouter.gpt-4.5-preview",
-              "azure.o3-mini",
-              "openrouter.claude-3.5-haiku",
               "azure.o1-mini",
               "azure.o1-mini",
-              "openrouter.o1",
-              "openrouter.gemini-2.5",
+              "openrouter.gemini-2.5-flash",
+              "claude-3-haiku",
+              "o1-mini",
+              "qwen-qwq",
               "llama-3.3-70b-versatile",
               "llama-3.3-70b-versatile",
-              "gpt-4.5-preview",
-              "openrouter.claude-3-opus",
               "openrouter.claude-3.5-sonnet",
               "openrouter.claude-3.5-sonnet",
+              "o3-mini",
               "o4-mini",
               "o4-mini",
-              "gemini-2.0-flash-lite",
-              "azure.gpt-4.5-preview",
+              "gpt-4.1",
+              "azure.o3-mini",
+              "openrouter.gpt-4.1-nano",
               "openrouter.gpt-4o",
               "openrouter.gpt-4o",
-              "o1",
+              "gemini-2.5",
               "azure.gpt-4o",
               "azure.gpt-4o",
-              "openrouter.gpt-4.1-nano",
+              "azure.gpt-4o-mini",
+              "claude-3.7-sonnet",
+              "azure.gpt-4.1-nano",
+              "openrouter.o1",
+              "openrouter.claude-3-haiku",
+              "bedrock.claude-3.7-sonnet",
+              "gemini-2.5-flash",
+              "azure.o3",
+              "openrouter.gemini-2.5",
+              "openrouter.o3",
+              "openrouter.o3-mini",
+              "openrouter.gpt-4.1-mini",
+              "openrouter.gpt-4.5-preview",
+              "openrouter.gpt-4o-mini",
+              "gpt-4.1-mini",
+              "meta-llama/llama-4-scout-17b-16e-instruct",
+              "openrouter.o1-mini",
+              "gpt-4.5-preview",
               "o3",
               "o3",
-              "gpt-4.1",
-              "azure.o1",
-              "claude-3-haiku",
+              "openrouter.claude-3.5-haiku",
               "claude-3-opus",
               "claude-3-opus",
-              "gpt-4.1-mini",
-              "openrouter.o4-mini",
-              "openrouter.gemini-2.5-flash",
-              "claude-3.5-haiku",
-              "o3-mini",
-              "azure.o3",
-              "gpt-4o",
-              "azure.gpt-4.1",
-              "openrouter.claude-3-haiku",
-              "gpt-4.1-nano",
-              "azure.gpt-4.1-nano",
-              "claude-3.7-sonnet",
-              "gemini-2.5",
+              "o1-pro",
+              "gemini-2.0-flash",
               "azure.o4-mini",
               "azure.o4-mini",
-              "o1-mini",
-              "qwen-qwq",
+              "openrouter.o4-mini",
+              "claude-3.5-sonnet",
               "meta-llama/llama-4-maverick-17b-128e-instruct",
               "meta-llama/llama-4-maverick-17b-128e-instruct",
+              "azure.o1",
               "openrouter.gpt-4.1",
               "openrouter.gpt-4.1",
               "openrouter.o1-pro",
               "openrouter.o1-pro",
-              "openrouter.o3",
-              "claude-3.5-sonnet",
-              "gemini-2.5-flash",
+              "gpt-4.1-nano",
+              "azure.gpt-4.5-preview",
+              "openrouter.claude-3-opus",
+              "gpt-4o-mini",
+              "o1",
+              "deepseek-r1-distill-llama-70b",
+              "azure.gpt-4.1",
+              "gpt-4o",
               "azure.gpt-4.1-mini",
               "azure.gpt-4.1-mini",
-              "openrouter.o3-mini"
+              "openrouter.claude-3.7-sonnet",
+              "claude-3.5-haiku",
+              "gemini-2.0-flash-lite"
             ],
             ],
             "type": "string"
             "type": "string"
           },
           },
@@ -354,6 +354,36 @@
     "tui": {
     "tui": {
       "description": "Terminal User Interface configuration",
       "description": "Terminal User Interface configuration",
       "properties": {
       "properties": {
+        "customTheme": {
+          "additionalProperties": {
+            "oneOf": [
+              {
+                "pattern": "^#[0-9a-fA-F]{6}$",
+                "type": "string"
+              },
+              {
+                "additionalProperties": false,
+                "properties": {
+                  "dark": {
+                    "pattern": "^#[0-9a-fA-F]{6}$",
+                    "type": "string"
+                  },
+                  "light": {
+                    "pattern": "^#[0-9a-fA-F]{6}$",
+                    "type": "string"
+                  }
+                },
+                "required": [
+                  "dark",
+                  "light"
+                ],
+                "type": "object"
+              }
+            ]
+          },
+          "description": "Custom theme color definitions",
+          "type": "object"
+        },
         "theme": {
         "theme": {
           "default": "opencode",
           "default": "opencode",
           "description": "TUI theme name",
           "description": "TUI theme name",
@@ -366,7 +396,8 @@
             "monokai",
             "monokai",
             "onedark",
             "onedark",
             "tokyonight",
             "tokyonight",
-            "tron"
+            "tron",
+            "custom"
           ],
           ],
           "type": "string"
           "type": "string"
         }
         }