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

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