Skip to content

Commit bd8c3cd

Browse files
committed
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
1 parent e5e9b3e commit bd8c3cd

File tree

8 files changed

+269
-119
lines changed

8 files changed

+269
-119
lines changed

README.md

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -61,66 +61,56 @@ The Models.dev dataset is also used to detect common environment variables like
6161

6262
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.
6363

64-
### Global Config
64+
### Config
6565

66-
Some basic configuration is available in the global config file.
66+
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.
6767

68-
```toml
69-
# ~/.config/opencode/config
70-
theme = "opencode"
71-
provider = "anthropic"
72-
model = "claude-sonnet-4-20250514"
73-
autoupdate = true
74-
75-
keybinds.leader = "ctrl+x"
76-
keybinds.session_new = "<leader>n"
77-
keybinds.editor_open = "<leader>e"
68+
```json title="opencode.json"
69+
{
70+
"$schema": "http://opencode.ai/config.json"
71+
"theme": "opencode",
72+
"model": "anthropic/claude-sonnet-4-20250514" // format is provider/model
73+
"autoshare": false,
74+
"autoupdate": true,
75+
}
7876
```
7977

8078
#### Keybinds
8179

82-
You can configure the keybinds in the global config file. (Note: values listed below are the defaults.)
83-
84-
```toml
85-
# ~/.config/opencode/config
86-
87-
[keybinds]
88-
leader = "ctrl+x"
89-
help = "<leader>h"
90-
editor_open = "<leader>e"
91-
session_new = "<leader>n"
92-
session_list = "<leader>l"
93-
session_share = "<leader>s"
94-
session_interrupt = "esc"
95-
session_compact = "<leader>c"
96-
tool_details = "<leader>d"
97-
model_list = "<leader>m"
98-
theme_list = "<leader>t"
99-
project_init = "<leader>i"
100-
input_clear = "ctrl+c"
101-
input_paste = "ctrl+v"
102-
input_submit = "enter"
103-
input_newline = "shift+enter"
104-
history_previous = "up"
105-
history_next = "down"
106-
messages_page_up = "pgup"
107-
messages_page_down = "pgdown"
108-
messages_half_page_up = "ctrl+alt+u"
109-
messages_half_page_down = "ctrl+alt+d"
110-
messages_previous = "ctrl+alt+k"
111-
messages_next = "ctrl+alt+j"
112-
messages_first = "ctrl+g"
113-
messages_last = "ctrl+alt+g"
114-
app_exit = "ctrl+c,<leader>q"
115-
```
116-
117-
### Project Config
118-
119-
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.
80+
You can configure custom keybinds, the values listed below are the defaults.
12081

12182
```json title="opencode.json"
12283
{
123-
"$schema": "http://opencode.ai/config.json"
84+
"$schema": "http://opencode.ai/config.json",
85+
"keybinds": {
86+
"leader": "ctrl+x",
87+
"help": "<leader>h",
88+
"editor_open": "<leader>e",
89+
"session_new": "<leader>n",
90+
"session_list": "<leader>l",
91+
"session_share": "<leader>s",
92+
"session_interrupt": "esc",
93+
"session_compact": "<leader>c",
94+
"tool_details": "<leader>d",
95+
"model_list": "<leader>m",
96+
"theme_list": "<leader>t",
97+
"project_init": "<leader>i",
98+
"input_clear": "ctrl+c",
99+
"input_paste": "ctrl+v",
100+
"input_submit": "enter",
101+
"input_newline": "shift+enter",
102+
"history_previous": "up",
103+
"history_next": "down",
104+
"messages_page_up": "pgup",
105+
"messages_page_down": "pgdown",
106+
"messages_half_page_up": "ctrl+alt+u",
107+
"messages_half_page_down": "ctrl+alt+d",
108+
"messages_previous": "ctrl+alt+k",
109+
"messages_next": "ctrl+alt+j",
110+
"messages_first": "ctrl+g",
111+
"messages_last": "ctrl+alt+g",
112+
"app_exit": "ctrl+c,<leader>q"
113+
}
124114
}
125115
```
126116

@@ -147,7 +137,7 @@ Project configuration is optional. You can place an `opencode.json` file in the
147137

148138
#### Providers
149139

150-
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.
140+
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)
151141

152142
```json title="opencode.json"
153143
{

packages/opencode/config.schema.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,23 @@
9595
"additionalProperties": false
9696
},
9797
"autoshare": {
98-
"type": "boolean"
98+
"type": "boolean",
99+
"description": "Share newly created sessions automatically"
99100
},
100101
"autoupdate": {
101-
"type": "boolean"
102+
"type": "boolean",
103+
"description": "Automatically update to the latest version"
102104
},
103105
"disabled_providers": {
104106
"type": "array",
105107
"items": {
106108
"type": "string"
107-
}
109+
},
110+
"description": "Disable providers that are loaded automatically"
111+
},
112+
"model": {
113+
"type": "string",
114+
"description": "Model to use in the format of provider/model, eg anthropic/claude-2"
108115
},
109116
"provider": {
110117
"type": "object",

packages/opencode/src/config/config.ts

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -64,44 +64,62 @@ export namespace Config {
6464
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
6565
export type Mcp = z.infer<typeof Mcp>
6666

67+
export const Keybinds = z
68+
.object({
69+
leader: z.string().optional(),
70+
help: z.string().optional(),
71+
editor_open: z.string().optional(),
72+
session_new: z.string().optional(),
73+
session_list: z.string().optional(),
74+
session_share: z.string().optional(),
75+
session_interrupt: z.string().optional(),
76+
session_compact: z.string().optional(),
77+
tool_details: z.string().optional(),
78+
model_list: z.string().optional(),
79+
theme_list: z.string().optional(),
80+
project_init: z.string().optional(),
81+
input_clear: z.string().optional(),
82+
input_paste: z.string().optional(),
83+
input_submit: z.string().optional(),
84+
input_newline: z.string().optional(),
85+
history_previous: z.string().optional(),
86+
history_next: z.string().optional(),
87+
messages_page_up: z.string().optional(),
88+
messages_page_down: z.string().optional(),
89+
messages_half_page_up: z.string().optional(),
90+
messages_half_page_down: z.string().optional(),
91+
messages_previous: z.string().optional(),
92+
messages_next: z.string().optional(),
93+
messages_first: z.string().optional(),
94+
messages_last: z.string().optional(),
95+
app_exit: z.string().optional(),
96+
})
97+
.openapi({
98+
ref: "Config.Keybinds",
99+
})
67100
export const Info = z
68101
.object({
69102
$schema: z.string().optional(),
70103
theme: z.string().optional(),
71-
keybinds: z
72-
.object({
73-
leader: z.string().optional(),
74-
help: z.string().optional(),
75-
editor_open: z.string().optional(),
76-
session_new: z.string().optional(),
77-
session_list: z.string().optional(),
78-
session_share: z.string().optional(),
79-
session_interrupt: z.string().optional(),
80-
session_compact: z.string().optional(),
81-
tool_details: z.string().optional(),
82-
model_list: z.string().optional(),
83-
theme_list: z.string().optional(),
84-
project_init: z.string().optional(),
85-
input_clear: z.string().optional(),
86-
input_paste: z.string().optional(),
87-
input_submit: z.string().optional(),
88-
input_newline: z.string().optional(),
89-
history_previous: z.string().optional(),
90-
history_next: z.string().optional(),
91-
messages_page_up: z.string().optional(),
92-
messages_page_down: z.string().optional(),
93-
messages_half_page_up: z.string().optional(),
94-
messages_half_page_down: z.string().optional(),
95-
messages_previous: z.string().optional(),
96-
messages_next: z.string().optional(),
97-
messages_first: z.string().optional(),
98-
messages_last: z.string().optional(),
99-
app_exit: z.string().optional(),
100-
})
104+
keybinds: Keybinds.optional(),
105+
autoshare: z
106+
.boolean()
107+
.optional()
108+
.describe("Share newly created sessions automatically"),
109+
autoupdate: z
110+
.boolean()
111+
.optional()
112+
.describe("Automatically update to the latest version"),
113+
disabled_providers: z
114+
.array(z.string())
115+
.optional()
116+
.describe("Disable providers that are loaded automatically"),
117+
model: z
118+
.string()
119+
.describe(
120+
"Model to use in the format of provider/model, eg anthropic/claude-2",
121+
)
101122
.optional(),
102-
autoshare: z.boolean().optional(),
103-
autoupdate: z.boolean().optional(),
104-
disabled_providers: z.array(z.string()).optional(),
105123
provider: z
106124
.record(
107125
ModelsDev.Provider.partial().extend({
@@ -130,9 +148,9 @@ export namespace Config {
130148
},
131149
})
132150
.then(async (mod) => {
133-
delete mod.default.provider
134-
delete mod.default.model
135-
result = mergeDeep(result, mod.default)
151+
const { provider, model, ...rest } = mod.default
152+
if (provider && model) result.model = `${provider}/${model}`
153+
result = mergeDeep(result, rest)
136154
await Bun.write(
137155
path.join(Global.Path.config, "config.json"),
138156
JSON.stringify(result, null, 2),

packages/tui/internal/app/app.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"path/filepath"
77
"sort"
8+
"strings"
89

910
"log/slog"
1011

@@ -61,8 +62,10 @@ func New(
6162
}
6263
configInfo := configResponse.JSON200
6364
if configInfo.Keybinds == nil {
64-
keybinds := make(map[string]string)
65-
keybinds["leader"] = "ctrl+x"
65+
leader := "ctrl+x"
66+
keybinds := client.ConfigKeybinds{
67+
Leader: &leader,
68+
}
6669
configInfo.Keybinds = &keybinds
6770
}
6871

@@ -76,6 +79,12 @@ func New(
7679
if configInfo.Theme != nil {
7780
appState.Theme = *configInfo.Theme
7881
}
82+
if configInfo.Model != nil {
83+
splits := strings.Split(*configInfo.Model, "/")
84+
appState.Provider = splits[0]
85+
appState.Model = strings.Join(splits[1:], "/")
86+
}
87+
7988
if appState.Theme != "" {
8089
theme.SetTheme(appState.Theme)
8190
}

packages/tui/internal/commands/command.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package commands
22

33
import (
4+
"encoding/json"
45
"slices"
56
"strings"
67

@@ -106,17 +107,6 @@ func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
106107
return false
107108
}
108109

109-
func (k Command) FromConfig(config *client.ConfigInfo) Command {
110-
if config.Keybinds == nil {
111-
return k
112-
}
113-
keybinds := *config.Keybinds
114-
if keybind, ok := keybinds[string(k.Name)]; ok {
115-
k.Keybindings = parseBindings(keybind)
116-
}
117-
return k
118-
}
119-
120110
func parseBindings(bindings ...string) []Keybinding {
121111
var parsedBindings []Keybinding
122112
for _, binding := range bindings {
@@ -278,8 +268,14 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
278268
},
279269
}
280270
registry := make(CommandRegistry)
271+
keybinds := map[string]string{}
272+
marshalled, _ := json.Marshal(*config.Keybinds)
273+
json.Unmarshal(marshalled, &keybinds)
281274
for _, command := range defaults {
282-
registry[command.Name] = command.FromConfig(config)
275+
if keybind, ok := keybinds[string(command.Name)]; ok {
276+
command.Keybindings = parseBindings(keybind)
277+
}
278+
registry[command.Name] = command
283279
}
284280
return registry
285281
}

packages/tui/internal/tui/tui.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,8 @@ func NewModel(app *app.App) tea.Model {
509509
messagesContainer := layout.NewContainer(messages)
510510

511511
var leaderBinding *key.Binding
512-
if leader, ok := (*app.Configg.Keybinds)["leader"]; ok {
513-
binding := key.NewBinding(key.WithKeys(leader))
512+
if (*app.Configg.Keybinds).Leader != nil {
513+
binding := key.NewBinding(key.WithKeys(*app.Configg.Keybinds.Leader))
514514
leaderBinding = &binding
515515
}
516516

0 commit comments

Comments
 (0)