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
This commit is contained in:
Dax Raad 2025-06-18 22:59:42 -04:00
parent e5e9b3e3c0
commit bd8c3cd0f1
8 changed files with 273 additions and 123 deletions

102
README.md
View file

@ -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.
```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"
```
#### 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.
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.
```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 custom keybinds, the values listed below are the defaults.
```json title="opencode.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"
{

View file

@ -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",

View file

@ -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),

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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": {

View file

@ -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"`