Jelajahi Sumber

BREAKING CONFIG CHANGE

We have changed the config format yet again - but this should be the
final time. You can see the readme for more details but the summary is

- got rid of global providers config
- got rid of global toml
- global config is now in `~/.config/opencode/config.json`
- it will be merged with any project level config
Dax Raad 8 bulan lalu
induk
melakukan
bd8c3cd0f1

+ 42 - 52
README.md

@@ -61,66 +61,56 @@ The Models.dev dataset is also used to detect common environment variables like
 
 If there are additional providers you want to use you can submit a PR to the [Models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below.
 
-### Global Config
+### Config
 
-Some basic configuration is available in the global config file.
+Config is optional and can be placed in the root of your repo or globally in `~/.config/opencode/config`. It can be checked in and shared with your team.
 
-```toml
-# ~/.config/opencode/config
-theme = "opencode"
-provider = "anthropic"
-model = "claude-sonnet-4-20250514"
-autoupdate = true
-
-keybinds.leader = "ctrl+x"
-keybinds.session_new = "<leader>n"
-keybinds.editor_open = "<leader>e"
+```json title="opencode.json"
+{
+  "$schema": "http://opencode.ai/config.json"
+  "theme": "opencode",
+  "model": "anthropic/claude-sonnet-4-20250514" // format is provider/model
+  "autoshare": false,
+  "autoupdate": true,
+}
 ```
 
 #### Keybinds
 
-You can configure the keybinds in the global config file. (Note: values listed below are the defaults.)
-
-```toml
-# ~/.config/opencode/config
-
-[keybinds]
-leader = "ctrl+x"
-help = "<leader>h"
-editor_open = "<leader>e"
-session_new = "<leader>n"
-session_list = "<leader>l"
-session_share = "<leader>s"
-session_interrupt = "esc"
-session_compact = "<leader>c"
-tool_details = "<leader>d"
-model_list = "<leader>m"
-theme_list = "<leader>t"
-project_init = "<leader>i"
-input_clear = "ctrl+c"
-input_paste = "ctrl+v"
-input_submit = "enter"
-input_newline = "shift+enter"
-history_previous = "up"
-history_next = "down"
-messages_page_up = "pgup"
-messages_page_down = "pgdown"
-messages_half_page_up = "ctrl+alt+u"
-messages_half_page_down = "ctrl+alt+d"
-messages_previous = "ctrl+alt+k"
-messages_next = "ctrl+alt+j"
-messages_first = "ctrl+g"
-messages_last = "ctrl+alt+g"
-app_exit = "ctrl+c,<leader>q"
-```
-
-### Project Config
-
-Project configuration is optional. You can place an `opencode.json` file in the root of your repo and is meant to be checked in and shared with your team.
+You can configure custom keybinds, the values listed below are the defaults.
 
 ```json title="opencode.json"
 {
-  "$schema": "http://opencode.ai/config.json"
+  "$schema": "http://opencode.ai/config.json",
+  "keybinds": {
+    "leader": "ctrl+x",
+    "help": "<leader>h",
+    "editor_open": "<leader>e",
+    "session_new": "<leader>n",
+    "session_list": "<leader>l",
+    "session_share": "<leader>s",
+    "session_interrupt": "esc",
+    "session_compact": "<leader>c",
+    "tool_details": "<leader>d",
+    "model_list": "<leader>m",
+    "theme_list": "<leader>t",
+    "project_init": "<leader>i",
+    "input_clear": "ctrl+c",
+    "input_paste": "ctrl+v",
+    "input_submit": "enter",
+    "input_newline": "shift+enter",
+    "history_previous": "up",
+    "history_next": "down",
+    "messages_page_up": "pgup",
+    "messages_page_down": "pgdown",
+    "messages_half_page_up": "ctrl+alt+u",
+    "messages_half_page_down": "ctrl+alt+d",
+    "messages_previous": "ctrl+alt+k",
+    "messages_next": "ctrl+alt+j",
+    "messages_first": "ctrl+g",
+    "messages_last": "ctrl+alt+g",
+    "app_exit": "ctrl+c,<leader>q"
+  }
 }
 ```
 
@@ -147,7 +137,7 @@ Project configuration is optional. You can place an `opencode.json` file in the
 
 #### Providers
 
-You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider.
+You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider. Remember most popular providers are preloaded from [models.dev](https://models.dev)
 
 ```json title="opencode.json"
 {

+ 10 - 3
packages/opencode/config.schema.json

@@ -95,16 +95,23 @@
       "additionalProperties": false
     },
     "autoshare": {
-      "type": "boolean"
+      "type": "boolean",
+      "description": "Share newly created sessions automatically"
     },
     "autoupdate": {
-      "type": "boolean"
+      "type": "boolean",
+      "description": "Automatically update to the latest version"
     },
     "disabled_providers": {
       "type": "array",
       "items": {
         "type": "string"
-      }
+      },
+      "description": "Disable providers that are loaded automatically"
+    },
+    "model": {
+      "type": "string",
+      "description": "Model to use in the format of provider/model, eg anthropic/claude-2"
     },
     "provider": {
       "type": "object",

+ 54 - 36
packages/opencode/src/config/config.ts

@@ -64,44 +64,62 @@ export namespace Config {
   export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
   export type Mcp = z.infer<typeof Mcp>
 
+  export const Keybinds = z
+    .object({
+      leader: z.string().optional(),
+      help: z.string().optional(),
+      editor_open: z.string().optional(),
+      session_new: z.string().optional(),
+      session_list: z.string().optional(),
+      session_share: z.string().optional(),
+      session_interrupt: z.string().optional(),
+      session_compact: z.string().optional(),
+      tool_details: z.string().optional(),
+      model_list: z.string().optional(),
+      theme_list: z.string().optional(),
+      project_init: z.string().optional(),
+      input_clear: z.string().optional(),
+      input_paste: z.string().optional(),
+      input_submit: z.string().optional(),
+      input_newline: z.string().optional(),
+      history_previous: z.string().optional(),
+      history_next: z.string().optional(),
+      messages_page_up: z.string().optional(),
+      messages_page_down: z.string().optional(),
+      messages_half_page_up: z.string().optional(),
+      messages_half_page_down: z.string().optional(),
+      messages_previous: z.string().optional(),
+      messages_next: z.string().optional(),
+      messages_first: z.string().optional(),
+      messages_last: z.string().optional(),
+      app_exit: z.string().optional(),
+    })
+    .openapi({
+      ref: "Config.Keybinds",
+    })
   export const Info = z
     .object({
       $schema: z.string().optional(),
       theme: z.string().optional(),
-      keybinds: z
-        .object({
-          leader: z.string().optional(),
-          help: z.string().optional(),
-          editor_open: z.string().optional(),
-          session_new: z.string().optional(),
-          session_list: z.string().optional(),
-          session_share: z.string().optional(),
-          session_interrupt: z.string().optional(),
-          session_compact: z.string().optional(),
-          tool_details: z.string().optional(),
-          model_list: z.string().optional(),
-          theme_list: z.string().optional(),
-          project_init: z.string().optional(),
-          input_clear: z.string().optional(),
-          input_paste: z.string().optional(),
-          input_submit: z.string().optional(),
-          input_newline: z.string().optional(),
-          history_previous: z.string().optional(),
-          history_next: z.string().optional(),
-          messages_page_up: z.string().optional(),
-          messages_page_down: z.string().optional(),
-          messages_half_page_up: z.string().optional(),
-          messages_half_page_down: z.string().optional(),
-          messages_previous: z.string().optional(),
-          messages_next: z.string().optional(),
-          messages_first: z.string().optional(),
-          messages_last: z.string().optional(),
-          app_exit: z.string().optional(),
-        })
+      keybinds: Keybinds.optional(),
+      autoshare: z
+        .boolean()
+        .optional()
+        .describe("Share newly created sessions automatically"),
+      autoupdate: z
+        .boolean()
+        .optional()
+        .describe("Automatically update to the latest version"),
+      disabled_providers: z
+        .array(z.string())
+        .optional()
+        .describe("Disable providers that are loaded automatically"),
+      model: z
+        .string()
+        .describe(
+          "Model to use in the format of provider/model, eg anthropic/claude-2",
+        )
         .optional(),
-      autoshare: z.boolean().optional(),
-      autoupdate: z.boolean().optional(),
-      disabled_providers: z.array(z.string()).optional(),
       provider: z
         .record(
           ModelsDev.Provider.partial().extend({
@@ -130,9 +148,9 @@ export namespace Config {
       },
     })
       .then(async (mod) => {
-        delete mod.default.provider
-        delete mod.default.model
-        result = mergeDeep(result, mod.default)
+        const { provider, model, ...rest } = mod.default
+        if (provider && model) result.model = `${provider}/${model}`
+        result = mergeDeep(result, rest)
         await Bun.write(
           path.join(Global.Path.config, "config.json"),
           JSON.stringify(result, null, 2),

+ 11 - 2
packages/tui/internal/app/app.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"path/filepath"
 	"sort"
+	"strings"
 
 	"log/slog"
 
@@ -61,8 +62,10 @@ func New(
 	}
 	configInfo := configResponse.JSON200
 	if configInfo.Keybinds == nil {
-		keybinds := make(map[string]string)
-		keybinds["leader"] = "ctrl+x"
+		leader := "ctrl+x"
+		keybinds := client.ConfigKeybinds{
+			Leader: &leader,
+		}
 		configInfo.Keybinds = &keybinds
 	}
 
@@ -76,6 +79,12 @@ func New(
 	if configInfo.Theme != nil {
 		appState.Theme = *configInfo.Theme
 	}
+	if configInfo.Model != nil {
+		splits := strings.Split(*configInfo.Model, "/")
+		appState.Provider = splits[0]
+		appState.Model = strings.Join(splits[1:], "/")
+	}
+
 	if appState.Theme != "" {
 		theme.SetTheme(appState.Theme)
 	}

+ 8 - 12
packages/tui/internal/commands/command.go

@@ -1,6 +1,7 @@
 package commands
 
 import (
+	"encoding/json"
 	"slices"
 	"strings"
 
@@ -106,17 +107,6 @@ func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
 	return false
 }
 
-func (k Command) FromConfig(config *client.ConfigInfo) Command {
-	if config.Keybinds == nil {
-		return k
-	}
-	keybinds := *config.Keybinds
-	if keybind, ok := keybinds[string(k.Name)]; ok {
-		k.Keybindings = parseBindings(keybind)
-	}
-	return k
-}
-
 func parseBindings(bindings ...string) []Keybinding {
 	var parsedBindings []Keybinding
 	for _, binding := range bindings {
@@ -278,8 +268,14 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
 		},
 	}
 	registry := make(CommandRegistry)
+	keybinds := map[string]string{}
+	marshalled, _ := json.Marshal(*config.Keybinds)
+	json.Unmarshal(marshalled, &keybinds)
 	for _, command := range defaults {
-		registry[command.Name] = command.FromConfig(config)
+		if keybind, ok := keybinds[string(command.Name)]; ok {
+			command.Keybindings = parseBindings(keybind)
+		}
+		registry[command.Name] = command
 	}
 	return registry
 }

+ 2 - 2
packages/tui/internal/tui/tui.go

@@ -509,8 +509,8 @@ func NewModel(app *app.App) tea.Model {
 	messagesContainer := layout.NewContainer(messages)
 
 	var leaderBinding *key.Binding
-	if leader, ok := (*app.Configg.Keybinds)["leader"]; ok {
-		binding := key.NewBinding(key.WithKeys(leader))
+	if (*app.Configg.Keybinds).Leader != nil {
+		binding := key.NewBinding(key.WithKeys(*app.Configg.Keybinds.Leader))
 		leaderBinding = &binding
 	}
 

+ 97 - 7
packages/tui/pkg/client/gen/openapi.json

@@ -1397,22 +1397,26 @@
             "type": "string"
           },
           "keybinds": {
-            "type": "object",
-            "additionalProperties": {
-              "type": "string"
-            }
+            "$ref": "#/components/schemas/Config.Keybinds"
           },
           "autoshare": {
-            "type": "boolean"
+            "type": "boolean",
+            "description": "Share newly created sessions automatically"
           },
           "autoupdate": {
-            "type": "boolean"
+            "type": "boolean",
+            "description": "Automatically update to the latest version"
           },
           "disabled_providers": {
             "type": "array",
             "items": {
               "type": "string"
-            }
+            },
+            "description": "Disable providers that are loaded automatically"
+          },
+          "model": {
+            "type": "string",
+            "description": "Model to use in the format of provider/model, eg anthropic/claude-2"
           },
           "provider": {
             "type": "object",
@@ -1528,6 +1532,92 @@
           }
         }
       },
+      "Config.Keybinds": {
+        "type": "object",
+        "properties": {
+          "leader": {
+            "type": "string"
+          },
+          "help": {
+            "type": "string"
+          },
+          "editor_open": {
+            "type": "string"
+          },
+          "session_new": {
+            "type": "string"
+          },
+          "session_list": {
+            "type": "string"
+          },
+          "session_share": {
+            "type": "string"
+          },
+          "session_interrupt": {
+            "type": "string"
+          },
+          "session_compact": {
+            "type": "string"
+          },
+          "tool_details": {
+            "type": "string"
+          },
+          "model_list": {
+            "type": "string"
+          },
+          "theme_list": {
+            "type": "string"
+          },
+          "project_init": {
+            "type": "string"
+          },
+          "input_clear": {
+            "type": "string"
+          },
+          "input_paste": {
+            "type": "string"
+          },
+          "input_submit": {
+            "type": "string"
+          },
+          "input_newline": {
+            "type": "string"
+          },
+          "history_previous": {
+            "type": "string"
+          },
+          "history_next": {
+            "type": "string"
+          },
+          "messages_page_up": {
+            "type": "string"
+          },
+          "messages_page_down": {
+            "type": "string"
+          },
+          "messages_half_page_up": {
+            "type": "string"
+          },
+          "messages_half_page_down": {
+            "type": "string"
+          },
+          "messages_previous": {
+            "type": "string"
+          },
+          "messages_next": {
+            "type": "string"
+          },
+          "messages_first": {
+            "type": "string"
+          },
+          "messages_last": {
+            "type": "string"
+          },
+          "app_exit": {
+            "type": "string"
+          }
+        }
+      },
       "Provider.Info": {
         "type": "object",
         "properties": {

+ 45 - 5
packages/tui/pkg/client/generated-client.go

@@ -41,13 +41,22 @@ type AppInfo struct {
 
 // ConfigInfo defines model for Config.Info.
 type ConfigInfo struct {
-	Schema            *string                                         `json:"$schema,omitempty"`
-	Autoshare         *bool                                           `json:"autoshare,omitempty"`
-	Autoupdate        *bool                                           `json:"autoupdate,omitempty"`
+	Schema *string `json:"$schema,omitempty"`
+
+	// Autoshare Share newly created sessions automatically
+	Autoshare *bool `json:"autoshare,omitempty"`
+
+	// Autoupdate Automatically update to the latest version
+	Autoupdate *bool `json:"autoupdate,omitempty"`
+
+	// DisabledProviders Disable providers that are loaded automatically
 	DisabledProviders *[]string                                       `json:"disabled_providers,omitempty"`
-	Keybinds          *map[string]string                              `json:"keybinds,omitempty"`
+	Keybinds          *ConfigKeybinds                                 `json:"keybinds,omitempty"`
 	Mcp               *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
-	Provider          *map[string]struct {
+
+	// Model Model to use in the format of provider/model, eg anthropic/claude-2
+	Model    *string `json:"model,omitempty"`
+	Provider *map[string]struct {
 		Api    *string   `json:"api,omitempty"`
 		Env    *[]string `json:"env,omitempty"`
 		Id     *string   `json:"id,omitempty"`
@@ -80,6 +89,37 @@ type ConfigInfo_Mcp_AdditionalProperties struct {
 	union json.RawMessage
 }
 
+// ConfigKeybinds defines model for Config.Keybinds.
+type ConfigKeybinds struct {
+	AppExit              *string `json:"app_exit,omitempty"`
+	EditorOpen           *string `json:"editor_open,omitempty"`
+	Help                 *string `json:"help,omitempty"`
+	HistoryNext          *string `json:"history_next,omitempty"`
+	HistoryPrevious      *string `json:"history_previous,omitempty"`
+	InputClear           *string `json:"input_clear,omitempty"`
+	InputNewline         *string `json:"input_newline,omitempty"`
+	InputPaste           *string `json:"input_paste,omitempty"`
+	InputSubmit          *string `json:"input_submit,omitempty"`
+	Leader               *string `json:"leader,omitempty"`
+	MessagesFirst        *string `json:"messages_first,omitempty"`
+	MessagesHalfPageDown *string `json:"messages_half_page_down,omitempty"`
+	MessagesHalfPageUp   *string `json:"messages_half_page_up,omitempty"`
+	MessagesLast         *string `json:"messages_last,omitempty"`
+	MessagesNext         *string `json:"messages_next,omitempty"`
+	MessagesPageDown     *string `json:"messages_page_down,omitempty"`
+	MessagesPageUp       *string `json:"messages_page_up,omitempty"`
+	MessagesPrevious     *string `json:"messages_previous,omitempty"`
+	ModelList            *string `json:"model_list,omitempty"`
+	ProjectInit          *string `json:"project_init,omitempty"`
+	SessionCompact       *string `json:"session_compact,omitempty"`
+	SessionInterrupt     *string `json:"session_interrupt,omitempty"`
+	SessionList          *string `json:"session_list,omitempty"`
+	SessionNew           *string `json:"session_new,omitempty"`
+	SessionShare         *string `json:"session_share,omitempty"`
+	ThemeList            *string `json:"theme_list,omitempty"`
+	ToolDetails          *string `json:"tool_details,omitempty"`
+}
+
 // ConfigMcpLocal defines model for Config.McpLocal.
 type ConfigMcpLocal struct {
 	Command     []string           `json:"command"`