From bd8c3cd0f1e30f5ed1cbf222415cef136edd14a9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 18 Jun 2025 22:59:42 -0400 Subject: [PATCH] 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 --- README.md | 102 +++++++++---------- packages/opencode/config.schema.json | 13 ++- packages/opencode/src/config/config.ts | 90 ++++++++++------- packages/tui/internal/app/app.go | 13 ++- packages/tui/internal/commands/command.go | 20 ++-- packages/tui/internal/tui/tui.go | 4 +- packages/tui/pkg/client/gen/openapi.json | 104 ++++++++++++++++++-- packages/tui/pkg/client/generated-client.go | 50 +++++++++- 8 files changed, 273 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 6439b8ab..8e42c2d0 100644 --- a/README.md +++ b/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. - -```toml -# ~/.config/opencode/config -theme = "opencode" -provider = "anthropic" -model = "claude-sonnet-4-20250514" -autoupdate = true - -keybinds.leader = "ctrl+x" -keybinds.session_new = "n" -keybinds.editor_open = "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 = "h" -editor_open = "e" -session_new = "n" -session_list = "l" -session_share = "s" -session_interrupt = "esc" -session_compact = "c" -tool_details = "d" -model_list = "m" -theme_list = "t" -project_init = "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,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": "h", + "editor_open": "e", + "session_new": "n", + "session_list": "l", + "session_share": "s", + "session_interrupt": "esc", + "session_compact": "c", + "tool_details": "d", + "model_list": "m", + "theme_list": "t", + "project_init": "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,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" { diff --git a/packages/opencode/config.schema.json b/packages/opencode/config.schema.json index 48978fcb..a57a4824 100644 --- a/packages/opencode/config.schema.json +++ b/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", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 565e8473..e1dc9ab7 100644 --- a/packages/opencode/src/config/config.ts +++ b/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 + 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), diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index eb4bd2da..72e6ad5e 100644 --- a/packages/tui/internal/app/app.go +++ b/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) } diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go index 6f628ae3..12378c0b 100644 --- a/packages/tui/internal/commands/command.go +++ b/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 } diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 5d63a161..25af7523 100644 --- a/packages/tui/internal/tui/tui.go +++ b/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 } diff --git a/packages/tui/pkg/client/gen/openapi.json b/packages/tui/pkg/client/gen/openapi.json index ea055a64..862a84d7 100644 --- a/packages/tui/pkg/client/gen/openapi.json +++ b/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": { diff --git a/packages/tui/pkg/client/generated-client.go b/packages/tui/pkg/client/generated-client.go index c2ecffe9..15acb548 100644 --- a/packages/tui/pkg/client/generated-client.go +++ b/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"`