Dax Raad 8 месяцев назад
Родитель
Сommit
e5e9b3e3c0

+ 108 - 5
packages/opencode/config.schema.json

@@ -4,11 +4,116 @@
     "$schema": {
       "type": "string"
     },
+    "theme": {
+      "type": "string"
+    },
+    "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"
+        }
+      },
+      "additionalProperties": false
+    },
+    "autoshare": {
+      "type": "boolean"
+    },
+    "autoupdate": {
+      "type": "boolean"
+    },
+    "disabled_providers": {
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
     "provider": {
       "type": "object",
       "additionalProperties": {
         "type": "object",
         "properties": {
+          "api": {
+            "type": "string"
+          },
           "name": {
             "type": "string"
           },
@@ -50,18 +155,16 @@
                     "output": {
                       "type": "number"
                     },
-                    "inputCached": {
+                    "cache_read": {
                       "type": "number"
                     },
-                    "outputCached": {
+                    "cache_write": {
                       "type": "number"
                     }
                   },
                   "required": [
                     "input",
-                    "output",
-                    "inputCached",
-                    "outputCached"
+                    "output"
                   ],
                   "additionalProperties": false
                 },

+ 2 - 2
packages/opencode/src/cli/cmd/run.ts

@@ -7,8 +7,8 @@ import { Share } from "../../share/share"
 import { Message } from "../../session/message"
 import { UI } from "../ui"
 import { cmd } from "./cmd"
-import { GlobalConfig } from "../../global/config"
 import { Flag } from "../../flag/flag"
+import { Config } from "../../config/config"
 
 const TOOL: Record<string, [string, string]> = {
   opencode_todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -60,7 +60,7 @@ export const RunCommand = cmd({
         UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
         UI.empty()
 
-        const cfg = await GlobalConfig.get()
+        const cfg = await Config.get()
         if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
           await Session.share(session.id)
           UI.println(

+ 83 - 14
packages/opencode/src/config/config.ts

@@ -6,15 +6,14 @@ import { Filesystem } from "../util/filesystem"
 import { ModelsDev } from "../provider/models"
 import { mergeDeep } from "remeda"
 import { Global } from "../global"
+import fs from "fs/promises"
+import { lazy } from "../util/lazy"
 
 export namespace Config {
   const log = Log.create({ service: "config" })
 
   export const state = App.state("config", async (app) => {
-    let result = await Bun.file(path.join(Global.Path.config, "config.json"))
-      .json()
-      .then((mod) => Info.parse(mod))
-      .catch(() => ({}) as Info)
+    let result = await global()
     for (const file of ["opencode.jsonc", "opencode.json"]) {
       const [resolved] = await Filesystem.findUp(
         file,
@@ -43,16 +42,24 @@ export namespace Config {
     return result
   })
 
-  export const McpLocal = z.object({
-    type: z.literal("local"),
-    command: z.string().array(),
-    environment: z.record(z.string(), z.string()).optional(),
-  })
+  export const McpLocal = z
+    .object({
+      type: z.literal("local"),
+      command: z.string().array(),
+      environment: z.record(z.string(), z.string()).optional(),
+    })
+    .openapi({
+      ref: "Config.McpLocal",
+    })
 
-  export const McpRemote = z.object({
-    type: z.literal("remote"),
-    url: z.string(),
-  })
+  export const McpRemote = z
+    .object({
+      type: z.literal("remote"),
+      url: z.string(),
+    })
+    .openapi({
+      ref: "Config.McpRemote",
+    })
 
   export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
   export type Mcp = z.infer<typeof Mcp>
@@ -60,6 +67,41 @@ export namespace Config {
   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(),
+        })
+        .optional(),
+      autoshare: z.boolean().optional(),
+      autoupdate: z.boolean().optional(),
+      disabled_providers: z.array(z.string()).optional(),
       provider: z
         .record(
           ModelsDev.Provider.partial().extend({
@@ -70,10 +112,37 @@ export namespace Config {
         .optional(),
       mcp: z.record(z.string(), Mcp).optional(),
     })
-    .strict()
+    .openapi({
+      ref: "Config.Info",
+    })
 
   export type Info = z.output<typeof Info>
 
+  export const global = lazy(async () => {
+    let result = await Bun.file(path.join(Global.Path.config, "config.json"))
+      .json()
+      .then((mod) => Info.parse(mod))
+      .catch(() => ({}) as Info)
+
+    await import(path.join(Global.Path.config, "config"), {
+      with: {
+        type: "toml",
+      },
+    })
+      .then(async (mod) => {
+        delete mod.default.provider
+        delete mod.default.model
+        result = mergeDeep(result, mod.default)
+        await Bun.write(
+          path.join(Global.Path.config, "config.json"),
+          JSON.stringify(result, null, 2),
+        )
+        await fs.unlink(path.join(Global.Path.config, "config"))
+      })
+      .catch(() => {})
+    return Info.parse(result)
+  })
+
   export function get() {
     return state()
   }

+ 0 - 26
packages/opencode/src/global/config.ts

@@ -1,26 +0,0 @@
-import { z } from "zod"
-import { Global } from "."
-import { lazy } from "../util/lazy"
-import path from "path"
-
-export namespace GlobalConfig {
-  export const Info = z.object({
-    provider: z.string().optional(),
-    model: z.string().optional(),
-    autoupdate: z.boolean().optional(),
-    autoshare: z.boolean().optional(),
-    disabled_providers: z.array(z.string()).optional(),
-  })
-  export type Info = z.infer<typeof Info>
-
-  export const get = lazy(async () => {
-    const toml = await import(path.join(Global.Path.config, "config"), {
-      with: {
-        type: "toml",
-      },
-    })
-      .then((mod) => mod.default)
-      .catch(() => ({}))
-    return Info.parse(toml)
-  })
-}

+ 2 - 2
packages/opencode/src/index.ts

@@ -15,9 +15,9 @@ import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
 import { UpgradeCommand } from "./cli/cmd/upgrade"
 import { Provider } from "./provider/provider"
 import { UI } from "./cli/ui"
-import { GlobalConfig } from "./global/config"
 import { Installation } from "./installation"
 import { Bus } from "./bus"
+import { Config } from "./config/config"
 
 const cli = yargs(hideBin(process.argv))
   .scriptName("opencode")
@@ -87,7 +87,7 @@ const cli = yargs(hideBin(process.argv))
           ;(async () => {
             if (Installation.VERSION === "dev") return
             if (Installation.isSnapshot()) return
-            const config = await GlobalConfig.get()
+            const config = await Config.global()
             if (config.autoupdate === false) return
             const latest = await Installation.latest()
             if (Installation.VERSION === latest) return

+ 2 - 3
packages/opencode/src/provider/provider.ts

@@ -24,7 +24,6 @@ import { ModelsDev } from "./models"
 import { NamedError } from "../util/error"
 import { Auth } from "../auth"
 import { TaskTool } from "../tool/task"
-import { GlobalConfig } from "../global/config"
 import { Global } from "../global"
 
 export namespace Provider {
@@ -179,7 +178,7 @@ export namespace Provider {
       database[providerID] = parsed
     }
 
-    const disabled = await GlobalConfig.get().then(
+    const disabled = await Config.get().then(
       (cfg) => new Set(cfg.disabled_providers ?? []),
     )
     // load env
@@ -300,7 +299,7 @@ export namespace Provider {
   }
 
   export async function defaultModel() {
-    const cfg = await GlobalConfig.get()
+    const cfg = await Config.get()
     const provider = await list()
       .then((val) => Object.values(val))
       .then((x) => x.find((p) => !cfg.provider || cfg.provider === p.info.id))

+ 20 - 0
packages/opencode/src/server/server.ts

@@ -15,6 +15,7 @@ import { NamedError } from "../util/error"
 import { ModelsDev } from "../provider/models"
 import { Ripgrep } from "../external/ripgrep"
 import { Installation } from "../installation"
+import { Config } from "../config/config"
 
 const ERRORS = {
   400: {
@@ -140,6 +141,25 @@ export namespace Server {
           return c.json(App.info())
         },
       )
+      .post(
+        "/config_get",
+        describeRoute({
+          description: "Get config info",
+          responses: {
+            200: {
+              description: "Get config info",
+              content: {
+                "application/json": {
+                  schema: resolver(Config.Info),
+                },
+              },
+            },
+          },
+        }),
+        async (c) => {
+          return c.json(await Config.get())
+        },
+      )
       .post(
         "/app_initialize",
         describeRoute({

+ 2 - 2
packages/opencode/src/session/index.ts

@@ -30,8 +30,8 @@ import type { Tool } from "../tool/tool"
 import { SystemPrompt } from "./system"
 import { Flag } from "../flag/flag"
 import type { ModelsDev } from "../provider/models"
-import { GlobalConfig } from "../global/config"
 import { Installation } from "../installation"
+import { Config } from "../config/config"
 
 export namespace Session {
   const log = Log.create({ service: "session" })
@@ -114,7 +114,7 @@ export namespace Session {
     log.info("created", result)
     state().sessions.set(result.id, result)
     await Storage.writeJSON("session/info/" + result.id, result)
-    const cfg = await GlobalConfig.get()
+    const cfg = await Config.get()
     if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.autoshare))
       share(result.id).then((share) => {
         update(result.id, (draft) => {

+ 22 - 16
packages/tui/internal/app/app.go

@@ -22,8 +22,9 @@ type App struct {
 	Info      client.AppInfo
 	Version   string
 	StatePath string
-	Config    *config.Config
+	Configg   *client.ConfigInfo
 	Client    *client.ClientWithResponses
+	State     *config.State
 	Provider  *client.ProviderInfo
 	Model     *client.ModelInfo
 	Session   *client.SessionInfo
@@ -54,14 +55,15 @@ func New(
 ) (*App, error) {
 	RootPath = appInfo.Path.Root
 
-	appConfigPath := filepath.Join(appInfo.Path.Config, "config")
-	appConfig, err := config.LoadConfig(appConfigPath)
+	configResponse, err := httpClient.PostConfigGetWithResponse(ctx)
 	if err != nil {
-		appConfig = config.NewConfig()
+		return nil, err
 	}
-	if len(appConfig.Keybinds) == 0 {
-		appConfig.Keybinds = make(map[string]string)
-		appConfig.Keybinds["leader"] = "ctrl+x"
+	configInfo := configResponse.JSON200
+	if configInfo.Keybinds == nil {
+		keybinds := make(map[string]string)
+		keybinds["leader"] = "ctrl+x"
+		configInfo.Keybinds = &keybinds
 	}
 
 	appStatePath := filepath.Join(appInfo.Path.State, "tui")
@@ -71,20 +73,25 @@ func New(
 		config.SaveState(appStatePath, appState)
 	}
 
-	mergedConfig := config.MergeState(appState, appConfig)
-	theme.SetTheme(mergedConfig.Theme)
+	if configInfo.Theme != nil {
+		appState.Theme = *configInfo.Theme
+	}
+	if appState.Theme != "" {
+		theme.SetTheme(appState.Theme)
+	}
 
-	slog.Debug("Loaded config", "config", mergedConfig)
+	slog.Debug("Loaded config", "config", configInfo)
 
 	app := &App{
 		Info:      appInfo,
 		Version:   version,
 		StatePath: appStatePath,
-		Config:    mergedConfig,
+		Configg:   configInfo,
+		State:     appState,
 		Client:    httpClient,
 		Session:   &client.SessionInfo{},
 		Messages:  []client.MessageInfo{},
-		Commands:  commands.LoadFromConfig(mergedConfig),
+		Commands:  commands.LoadFromConfig(configInfo),
 	}
 
 	return app, nil
@@ -130,11 +137,11 @@ func (a *App) InitializeProvider() tea.Cmd {
 		var currentProvider *client.ProviderInfo
 		var currentModel *client.ModelInfo
 		for _, provider := range providers {
-			if provider.Id == a.Config.Provider {
+			if provider.Id == a.State.Provider {
 				currentProvider = &provider
 
 				for _, model := range provider.Models {
-					if model.Id == a.Config.Model {
+					if model.Id == a.State.Model {
 						currentModel = &model
 					}
 				}
@@ -182,8 +189,7 @@ func (a *App) IsBusy() bool {
 }
 
 func (a *App) SaveState() {
-	state := config.ConfigToState(a.Config)
-	err := config.SaveState(a.StatePath, state)
+	err := config.SaveState(a.StatePath, a.State)
 	if err != nil {
 		slog.Error("Failed to save state", "error", err)
 	}

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

@@ -5,7 +5,7 @@ import (
 	"strings"
 
 	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/sst/opencode/internal/config"
+	"github.com/sst/opencode/pkg/client"
 )
 
 type ExecuteCommandMsg Command
@@ -106,8 +106,12 @@ func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
 	return false
 }
 
-func (k Command) FromConfig(config *config.Config) Command {
-	if keybind, ok := config.Keybinds[string(k.Name)]; ok {
+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
@@ -129,7 +133,7 @@ func parseBindings(bindings ...string) []Keybinding {
 	return parsedBindings
 }
 
-func LoadFromConfig(config *config.Config) CommandRegistry {
+func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
 	defaults := []Command{
 		{
 			Name:        AppHelpCommand,

+ 4 - 45
packages/tui/internal/config/config.go

@@ -7,6 +7,7 @@ import (
 	"os"
 
 	"github.com/BurntSushi/toml"
+	"github.com/sst/opencode/pkg/client"
 )
 
 type State struct {
@@ -15,44 +16,15 @@ type State struct {
 	Model    string `toml:"model"`
 }
 
-type Config struct {
-	Theme    string            `toml:"theme"`
-	Provider string            `toml:"provider"`
-	Model    string            `toml:"model"`
-	Keybinds map[string]string `toml:"keybinds"`
-}
-
 func NewState() *State {
 	return &State{
 		Theme: "opencode",
 	}
 }
 
-func NewConfig() *Config {
-	keybinds := make(map[string]string)
-	keybinds["leader"] = "ctrl+x"
-	return &Config{
-		Keybinds: keybinds,
-	}
-}
-
-func ConfigToState(config *Config) *State {
-	return &State{
-		Theme:    config.Theme,
-		Provider: config.Provider,
-		Model:    config.Model,
-	}
-}
-
-func MergeState(state *State, config *Config) *Config {
-	if config.Theme == "" {
-		config.Theme = state.Theme
-	}
-	if config.Provider == "" {
-		config.Provider = state.Provider
-	}
-	if config.Model == "" {
-		config.Model = state.Model
+func MergeState(state *State, config *client.ConfigInfo) *client.ConfigInfo {
+	if config.Theme == nil {
+		config.Theme = &state.Theme
 	}
 	return config
 }
@@ -79,19 +51,6 @@ func SaveState(filePath string, state *State) error {
 	return nil
 }
 
-// LoadConfig reads a Config struct from the specified TOML file.
-// It returns a pointer to the Config struct and an error if any issues occur.
-func LoadConfig(filePath string) (*Config, error) {
-	var config Config
-	if _, err := toml.DecodeFile(filePath, &config); err != nil {
-		if _, statErr := os.Stat(filePath); os.IsNotExist(statErr) {
-			return nil, fmt.Errorf("config file not found at %s: %w", filePath, statErr)
-		}
-		return nil, fmt.Errorf("failed to decode TOML from file %s: %w", filePath, err)
-	}
-	return &config, nil
-}
-
 // LoadState loads the state from the specified TOML file.
 // It returns a pointer to the State struct and an error if any issues occur.
 func LoadState(filePath string) (*State, error) {

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

@@ -249,11 +249,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case app.ModelSelectedMsg:
 		a.app.Provider = &msg.Provider
 		a.app.Model = &msg.Model
-		a.app.Config.Provider = msg.Provider.Id
-		a.app.Config.Model = msg.Model.Id
+		a.app.State.Provider = msg.Provider.Id
+		a.app.State.Model = msg.Model.Id
 		a.app.SaveState()
 	case dialog.ThemeSelectedMsg:
-		a.app.Config.Theme = msg.ThemeName
+		a.app.State.Theme = msg.ThemeName
 		a.app.SaveState()
 	}
 
@@ -509,7 +509,7 @@ func NewModel(app *app.App) tea.Model {
 	messagesContainer := layout.NewContainer(messages)
 
 	var leaderBinding *key.Binding
-	if leader, ok := app.Config.Keybinds["leader"]; ok {
+	if leader, ok := (*app.Configg.Keybinds)["leader"]; ok {
 		binding := key.NewBinding(key.WithKeys(leader))
 		leaderBinding = &binding
 	}

+ 247 - 45
packages/tui/pkg/client/gen/openapi.json

@@ -44,6 +44,25 @@
         "description": "Get app info"
       }
     },
+    "/config_get": {
+      "post": {
+        "responses": {
+          "200": {
+            "description": "Get config info",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Config.Info"
+                }
+              }
+            }
+          }
+        },
+        "operationId": "postConfig_get",
+        "parameters": [],
+        "description": "Get config info"
+      }
+    },
     "/app_initialize": {
       "post": {
         "responses": {
@@ -519,13 +538,13 @@
             "$ref": "#/components/schemas/Event.message.part.updated"
           },
           {
-            "$ref": "#/components/schemas/Event.session.updated"
+            "$ref": "#/components/schemas/Event.installation.updated"
           },
           {
-            "$ref": "#/components/schemas/Event.session.error"
+            "$ref": "#/components/schemas/Event.session.updated"
           },
           {
-            "$ref": "#/components/schemas/Event.installation.updated"
+            "$ref": "#/components/schemas/Event.session.error"
           }
         ],
         "discriminator": {
@@ -536,9 +555,9 @@
             "permission.updated": "#/components/schemas/Event.permission.updated",
             "message.updated": "#/components/schemas/Event.message.updated",
             "message.part.updated": "#/components/schemas/Event.message.part.updated",
+            "installation.updated": "#/components/schemas/Event.installation.updated",
             "session.updated": "#/components/schemas/Event.session.updated",
-            "session.error": "#/components/schemas/Event.session.error",
-            "installation.updated": "#/components/schemas/Event.installation.updated"
+            "session.error": "#/components/schemas/Event.session.error"
           }
         }
       },
@@ -1182,6 +1201,30 @@
           "properties"
         ]
       },
+      "Event.installation.updated": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "installation.updated"
+          },
+          "properties": {
+            "type": "object",
+            "properties": {
+              "version": {
+                "type": "string"
+              }
+            },
+            "required": [
+              "version"
+            ]
+          }
+        },
+        "required": [
+          "type",
+          "properties"
+        ]
+      },
       "Event.session.updated": {
         "type": "object",
         "properties": {
@@ -1220,21 +1263,20 @@
           "share": {
             "type": "object",
             "properties": {
-              "secret": {
-                "type": "string"
-              },
               "url": {
                 "type": "string"
               }
             },
             "required": [
-              "secret",
               "url"
             ]
           },
           "title": {
             "type": "string"
           },
+          "version": {
+            "type": "string"
+          },
           "time": {
             "type": "object",
             "properties": {
@@ -1254,6 +1296,7 @@
         "required": [
           "id",
           "title",
+          "version",
           "time"
         ]
       },
@@ -1292,30 +1335,6 @@
           "properties"
         ]
       },
-      "Event.installation.updated": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "installation.updated"
-          },
-          "properties": {
-            "type": "object",
-            "properties": {
-              "version": {
-                "type": "string"
-              }
-            },
-            "required": [
-              "version"
-            ]
-          }
-        },
-        "required": [
-          "type",
-          "properties"
-        ]
-      },
       "App.Info": {
         "type": "object",
         "properties": {
@@ -1368,21 +1387,153 @@
           "time"
         ]
       },
-      "Error": {
+      "Config.Info": {
         "type": "object",
         "properties": {
-          "data": {
+          "$schema": {
+            "type": "string"
+          },
+          "theme": {
+            "type": "string"
+          },
+          "keybinds": {
             "type": "object",
-            "additionalProperties": {}
+            "additionalProperties": {
+              "type": "string"
+            }
+          },
+          "autoshare": {
+            "type": "boolean"
+          },
+          "autoupdate": {
+            "type": "boolean"
+          },
+          "disabled_providers": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "provider": {
+            "type": "object",
+            "additionalProperties": {
+              "type": "object",
+              "properties": {
+                "api": {
+                  "type": "string"
+                },
+                "name": {
+                  "type": "string"
+                },
+                "env": {
+                  "type": "array",
+                  "items": {
+                    "type": "string"
+                  }
+                },
+                "id": {
+                  "type": "string"
+                },
+                "npm": {
+                  "type": "string"
+                },
+                "models": {
+                  "type": "object",
+                  "additionalProperties": {
+                    "type": "object",
+                    "properties": {
+                      "name": {
+                        "type": "string"
+                      },
+                      "attachment": {
+                        "type": "boolean"
+                      },
+                      "reasoning": {
+                        "type": "boolean"
+                      },
+                      "temperature": {
+                        "type": "boolean"
+                      },
+                      "cost": {
+                        "type": "object",
+                        "properties": {
+                          "input": {
+                            "type": "number"
+                          },
+                          "output": {
+                            "type": "number"
+                          },
+                          "cache_read": {
+                            "type": "number"
+                          },
+                          "cache_write": {
+                            "type": "number"
+                          }
+                        },
+                        "required": [
+                          "input",
+                          "output"
+                        ]
+                      },
+                      "limit": {
+                        "type": "object",
+                        "properties": {
+                          "context": {
+                            "type": "number"
+                          },
+                          "output": {
+                            "type": "number"
+                          }
+                        },
+                        "required": [
+                          "context",
+                          "output"
+                        ]
+                      },
+                      "id": {
+                        "type": "string"
+                      }
+                    }
+                  }
+                },
+                "options": {
+                  "type": "object",
+                  "additionalProperties": {}
+                }
+              },
+              "required": [
+                "models"
+              ]
+            }
+          },
+          "mcp": {
+            "type": "object",
+            "additionalProperties": {
+              "oneOf": [
+                {
+                  "$ref": "#/components/schemas/Config.McpLocal"
+                },
+                {
+                  "$ref": "#/components/schemas/Config.McpRemote"
+                }
+              ],
+              "discriminator": {
+                "propertyName": "type",
+                "mapping": {
+                  "local": "#/components/schemas/Config.McpLocal",
+                  "remote": "#/components/schemas/Config.McpRemote"
+                }
+              }
+            }
           }
-        },
-        "required": [
-          "data"
-        ]
+        }
       },
       "Provider.Info": {
         "type": "object",
         "properties": {
+          "api": {
+            "type": "string"
+          },
           "name": {
             "type": "string"
           },
@@ -1436,18 +1587,16 @@
               "output": {
                 "type": "number"
               },
-              "inputCached": {
+              "cache_read": {
                 "type": "number"
               },
-              "outputCached": {
+              "cache_write": {
                 "type": "number"
               }
             },
             "required": [
               "input",
-              "output",
-              "inputCached",
-              "outputCached"
+              "output"
             ]
           },
           "limit": {
@@ -1479,6 +1628,59 @@
           "id"
         ]
       },
+      "Config.McpLocal": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "local"
+          },
+          "command": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "environment": {
+            "type": "object",
+            "additionalProperties": {
+              "type": "string"
+            }
+          }
+        },
+        "required": [
+          "type",
+          "command"
+        ]
+      },
+      "Config.McpRemote": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "remote"
+          },
+          "url": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "type",
+          "url"
+        ]
+      },
+      "Error": {
+        "type": "object",
+        "properties": {
+          "data": {
+            "type": "object",
+            "additionalProperties": {}
+          }
+        },
+        "required": [
+          "data"
+        ]
+      },
       "InstallationInfo": {
         "type": "object",
         "properties": {

+ 280 - 34
packages/tui/pkg/client/generated-client.go

@@ -39,6 +39,60 @@ type AppInfo struct {
 	User string `json:"user"`
 }
 
+// ConfigInfo defines model for Config.Info.
+type ConfigInfo struct {
+	Schema            *string                                         `json:"$schema,omitempty"`
+	Autoshare         *bool                                           `json:"autoshare,omitempty"`
+	Autoupdate        *bool                                           `json:"autoupdate,omitempty"`
+	DisabledProviders *[]string                                       `json:"disabled_providers,omitempty"`
+	Keybinds          *map[string]string                              `json:"keybinds,omitempty"`
+	Mcp               *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
+	Provider          *map[string]struct {
+		Api    *string   `json:"api,omitempty"`
+		Env    *[]string `json:"env,omitempty"`
+		Id     *string   `json:"id,omitempty"`
+		Models map[string]struct {
+			Attachment *bool `json:"attachment,omitempty"`
+			Cost       *struct {
+				CacheRead  *float32 `json:"cache_read,omitempty"`
+				CacheWrite *float32 `json:"cache_write,omitempty"`
+				Input      float32  `json:"input"`
+				Output     float32  `json:"output"`
+			} `json:"cost,omitempty"`
+			Id    *string `json:"id,omitempty"`
+			Limit *struct {
+				Context float32 `json:"context"`
+				Output  float32 `json:"output"`
+			} `json:"limit,omitempty"`
+			Name        *string `json:"name,omitempty"`
+			Reasoning   *bool   `json:"reasoning,omitempty"`
+			Temperature *bool   `json:"temperature,omitempty"`
+		} `json:"models"`
+		Name    *string                 `json:"name,omitempty"`
+		Npm     *string                 `json:"npm,omitempty"`
+		Options *map[string]interface{} `json:"options,omitempty"`
+	} `json:"provider,omitempty"`
+	Theme *string `json:"theme,omitempty"`
+}
+
+// ConfigInfo_Mcp_AdditionalProperties defines model for Config.Info.mcp.AdditionalProperties.
+type ConfigInfo_Mcp_AdditionalProperties struct {
+	union json.RawMessage
+}
+
+// ConfigMcpLocal defines model for Config.McpLocal.
+type ConfigMcpLocal struct {
+	Command     []string           `json:"command"`
+	Environment *map[string]string `json:"environment,omitempty"`
+	Type        string             `json:"type"`
+}
+
+// ConfigMcpRemote defines model for Config.McpRemote.
+type ConfigMcpRemote struct {
+	Type string `json:"type"`
+	Url  string `json:"url"`
+}
+
 // Error defines model for Error.
 type Error struct {
 	Data map[string]interface{} `json:"data"`
@@ -263,10 +317,10 @@ type MessageToolInvocationToolResult struct {
 type ModelInfo struct {
 	Attachment bool `json:"attachment"`
 	Cost       struct {
-		Input        float32 `json:"input"`
-		InputCached  float32 `json:"inputCached"`
-		Output       float32 `json:"output"`
-		OutputCached float32 `json:"outputCached"`
+		CacheRead  *float32 `json:"cache_read,omitempty"`
+		CacheWrite *float32 `json:"cache_write,omitempty"`
+		Input      float32  `json:"input"`
+		Output     float32  `json:"output"`
 	} `json:"cost"`
 	Id    string `json:"id"`
 	Limit struct {
@@ -280,6 +334,7 @@ type ModelInfo struct {
 
 // ProviderInfo defines model for Provider.Info.
 type ProviderInfo struct {
+	Api    *string              `json:"api,omitempty"`
 	Env    []string             `json:"env"`
 	Id     string               `json:"id"`
 	Models map[string]ModelInfo `json:"models"`
@@ -320,14 +375,14 @@ type SessionInfo struct {
 	Id       string  `json:"id"`
 	ParentID *string `json:"parentID,omitempty"`
 	Share    *struct {
-		Secret string `json:"secret"`
-		Url    string `json:"url"`
+		Url string `json:"url"`
 	} `json:"share,omitempty"`
 	Time struct {
 		Created float32 `json:"created"`
 		Updated float32 `json:"updated"`
 	} `json:"time"`
-	Title string `json:"title"`
+	Title   string `json:"title"`
+	Version string `json:"version"`
 }
 
 // PostFileSearchJSONBody defines parameters for PostFileSearch.
@@ -472,6 +527,95 @@ func (a MessageInfo_Metadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, e
 	return json.Marshal(object)
 }
 
+// AsConfigMcpLocal returns the union data inside the ConfigInfo_Mcp_AdditionalProperties as a ConfigMcpLocal
+func (t ConfigInfo_Mcp_AdditionalProperties) AsConfigMcpLocal() (ConfigMcpLocal, error) {
+	var body ConfigMcpLocal
+	err := json.Unmarshal(t.union, &body)
+	return body, err
+}
+
+// FromConfigMcpLocal overwrites any union data inside the ConfigInfo_Mcp_AdditionalProperties as the provided ConfigMcpLocal
+func (t *ConfigInfo_Mcp_AdditionalProperties) FromConfigMcpLocal(v ConfigMcpLocal) error {
+	v.Type = "local"
+	b, err := json.Marshal(v)
+	t.union = b
+	return err
+}
+
+// MergeConfigMcpLocal performs a merge with any union data inside the ConfigInfo_Mcp_AdditionalProperties, using the provided ConfigMcpLocal
+func (t *ConfigInfo_Mcp_AdditionalProperties) MergeConfigMcpLocal(v ConfigMcpLocal) error {
+	v.Type = "local"
+	b, err := json.Marshal(v)
+	if err != nil {
+		return err
+	}
+
+	merged, err := runtime.JSONMerge(t.union, b)
+	t.union = merged
+	return err
+}
+
+// AsConfigMcpRemote returns the union data inside the ConfigInfo_Mcp_AdditionalProperties as a ConfigMcpRemote
+func (t ConfigInfo_Mcp_AdditionalProperties) AsConfigMcpRemote() (ConfigMcpRemote, error) {
+	var body ConfigMcpRemote
+	err := json.Unmarshal(t.union, &body)
+	return body, err
+}
+
+// FromConfigMcpRemote overwrites any union data inside the ConfigInfo_Mcp_AdditionalProperties as the provided ConfigMcpRemote
+func (t *ConfigInfo_Mcp_AdditionalProperties) FromConfigMcpRemote(v ConfigMcpRemote) error {
+	v.Type = "remote"
+	b, err := json.Marshal(v)
+	t.union = b
+	return err
+}
+
+// MergeConfigMcpRemote performs a merge with any union data inside the ConfigInfo_Mcp_AdditionalProperties, using the provided ConfigMcpRemote
+func (t *ConfigInfo_Mcp_AdditionalProperties) MergeConfigMcpRemote(v ConfigMcpRemote) error {
+	v.Type = "remote"
+	b, err := json.Marshal(v)
+	if err != nil {
+		return err
+	}
+
+	merged, err := runtime.JSONMerge(t.union, b)
+	t.union = merged
+	return err
+}
+
+func (t ConfigInfo_Mcp_AdditionalProperties) Discriminator() (string, error) {
+	var discriminator struct {
+		Discriminator string `json:"type"`
+	}
+	err := json.Unmarshal(t.union, &discriminator)
+	return discriminator.Discriminator, err
+}
+
+func (t ConfigInfo_Mcp_AdditionalProperties) ValueByDiscriminator() (interface{}, error) {
+	discriminator, err := t.Discriminator()
+	if err != nil {
+		return nil, err
+	}
+	switch discriminator {
+	case "local":
+		return t.AsConfigMcpLocal()
+	case "remote":
+		return t.AsConfigMcpRemote()
+	default:
+		return nil, errors.New("unknown discriminator value: " + discriminator)
+	}
+}
+
+func (t ConfigInfo_Mcp_AdditionalProperties) MarshalJSON() ([]byte, error) {
+	b, err := t.union.MarshalJSON()
+	return b, err
+}
+
+func (t *ConfigInfo_Mcp_AdditionalProperties) UnmarshalJSON(b []byte) error {
+	err := t.union.UnmarshalJSON(b)
+	return err
+}
+
 // AsEventStorageWrite returns the union data inside the Event as a EventStorageWrite
 func (t Event) AsEventStorageWrite() (EventStorageWrite, error) {
 	var body EventStorageWrite
@@ -612,24 +756,24 @@ func (t *Event) MergeEventMessagePartUpdated(v EventMessagePartUpdated) error {
 	return err
 }
 
-// AsEventSessionUpdated returns the union data inside the Event as a EventSessionUpdated
-func (t Event) AsEventSessionUpdated() (EventSessionUpdated, error) {
-	var body EventSessionUpdated
+// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
+func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
+	var body EventInstallationUpdated
 	err := json.Unmarshal(t.union, &body)
 	return body, err
 }
 
-// FromEventSessionUpdated overwrites any union data inside the Event as the provided EventSessionUpdated
-func (t *Event) FromEventSessionUpdated(v EventSessionUpdated) error {
-	v.Type = "session.updated"
+// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
+func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
+	v.Type = "installation.updated"
 	b, err := json.Marshal(v)
 	t.union = b
 	return err
 }
 
-// MergeEventSessionUpdated performs a merge with any union data inside the Event, using the provided EventSessionUpdated
-func (t *Event) MergeEventSessionUpdated(v EventSessionUpdated) error {
-	v.Type = "session.updated"
+// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
+func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
+	v.Type = "installation.updated"
 	b, err := json.Marshal(v)
 	if err != nil {
 		return err
@@ -640,24 +784,24 @@ func (t *Event) MergeEventSessionUpdated(v EventSessionUpdated) error {
 	return err
 }
 
-// AsEventSessionError returns the union data inside the Event as a EventSessionError
-func (t Event) AsEventSessionError() (EventSessionError, error) {
-	var body EventSessionError
+// AsEventSessionUpdated returns the union data inside the Event as a EventSessionUpdated
+func (t Event) AsEventSessionUpdated() (EventSessionUpdated, error) {
+	var body EventSessionUpdated
 	err := json.Unmarshal(t.union, &body)
 	return body, err
 }
 
-// FromEventSessionError overwrites any union data inside the Event as the provided EventSessionError
-func (t *Event) FromEventSessionError(v EventSessionError) error {
-	v.Type = "session.error"
+// FromEventSessionUpdated overwrites any union data inside the Event as the provided EventSessionUpdated
+func (t *Event) FromEventSessionUpdated(v EventSessionUpdated) error {
+	v.Type = "session.updated"
 	b, err := json.Marshal(v)
 	t.union = b
 	return err
 }
 
-// MergeEventSessionError performs a merge with any union data inside the Event, using the provided EventSessionError
-func (t *Event) MergeEventSessionError(v EventSessionError) error {
-	v.Type = "session.error"
+// MergeEventSessionUpdated performs a merge with any union data inside the Event, using the provided EventSessionUpdated
+func (t *Event) MergeEventSessionUpdated(v EventSessionUpdated) error {
+	v.Type = "session.updated"
 	b, err := json.Marshal(v)
 	if err != nil {
 		return err
@@ -668,24 +812,24 @@ func (t *Event) MergeEventSessionError(v EventSessionError) error {
 	return err
 }
 
-// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
-func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
-	var body EventInstallationUpdated
+// AsEventSessionError returns the union data inside the Event as a EventSessionError
+func (t Event) AsEventSessionError() (EventSessionError, error) {
+	var body EventSessionError
 	err := json.Unmarshal(t.union, &body)
 	return body, err
 }
 
-// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
-func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
-	v.Type = "installation.updated"
+// FromEventSessionError overwrites any union data inside the Event as the provided EventSessionError
+func (t *Event) FromEventSessionError(v EventSessionError) error {
+	v.Type = "session.error"
 	b, err := json.Marshal(v)
 	t.union = b
 	return err
 }
 
-// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
-func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
-	v.Type = "installation.updated"
+// MergeEventSessionError performs a merge with any union data inside the Event, using the provided EventSessionError
+func (t *Event) MergeEventSessionError(v EventSessionError) error {
+	v.Type = "session.error"
 	b, err := json.Marshal(v)
 	if err != nil {
 		return err
@@ -1326,6 +1470,9 @@ type ClientInterface interface {
 	// PostAppInitialize request
 	PostAppInitialize(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
 
+	// PostConfigGet request
+	PostConfigGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
+
 	// GetEvent request
 	GetEvent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
 
@@ -1404,6 +1551,18 @@ func (c *Client) PostAppInitialize(ctx context.Context, reqEditors ...RequestEdi
 	return c.Client.Do(req)
 }
 
+func (c *Client) PostConfigGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
+	req, err := NewPostConfigGetRequest(c.Server)
+	if err != nil {
+		return nil, err
+	}
+	req = req.WithContext(ctx)
+	if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+		return nil, err
+	}
+	return c.Client.Do(req)
+}
+
 func (c *Client) GetEvent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
 	req, err := NewGetEventRequest(c.Server)
 	if err != nil {
@@ -1698,6 +1857,33 @@ func NewPostAppInitializeRequest(server string) (*http.Request, error) {
 	return req, nil
 }
 
+// NewPostConfigGetRequest generates requests for PostConfigGet
+func NewPostConfigGetRequest(server string) (*http.Request, error) {
+	var err error
+
+	serverURL, err := url.Parse(server)
+	if err != nil {
+		return nil, err
+	}
+
+	operationPath := fmt.Sprintf("/config_get")
+	if operationPath[0] == '/' {
+		operationPath = "." + operationPath
+	}
+
+	queryURL, err := serverURL.Parse(operationPath)
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest("POST", queryURL.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return req, nil
+}
+
 // NewGetEventRequest generates requests for GetEvent
 func NewGetEventRequest(server string) (*http.Request, error) {
 	var err error
@@ -2189,6 +2375,9 @@ type ClientWithResponsesInterface interface {
 	// PostAppInitializeWithResponse request
 	PostAppInitializeWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostAppInitializeResponse, error)
 
+	// PostConfigGetWithResponse request
+	PostConfigGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostConfigGetResponse, error)
+
 	// GetEventWithResponse request
 	GetEventWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetEventResponse, error)
 
@@ -2287,6 +2476,28 @@ func (r PostAppInitializeResponse) StatusCode() int {
 	return 0
 }
 
+type PostConfigGetResponse struct {
+	Body         []byte
+	HTTPResponse *http.Response
+	JSON200      *ConfigInfo
+}
+
+// Status returns HTTPResponse.Status
+func (r PostConfigGetResponse) Status() string {
+	if r.HTTPResponse != nil {
+		return r.HTTPResponse.Status
+	}
+	return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r PostConfigGetResponse) StatusCode() int {
+	if r.HTTPResponse != nil {
+		return r.HTTPResponse.StatusCode
+	}
+	return 0
+}
+
 type GetEventResponse struct {
 	Body         []byte
 	HTTPResponse *http.Response
@@ -2600,6 +2811,15 @@ func (c *ClientWithResponses) PostAppInitializeWithResponse(ctx context.Context,
 	return ParsePostAppInitializeResponse(rsp)
 }
 
+// PostConfigGetWithResponse request returning *PostConfigGetResponse
+func (c *ClientWithResponses) PostConfigGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostConfigGetResponse, error) {
+	rsp, err := c.PostConfigGet(ctx, reqEditors...)
+	if err != nil {
+		return nil, err
+	}
+	return ParsePostConfigGetResponse(rsp)
+}
+
 // GetEventWithResponse request returning *GetEventResponse
 func (c *ClientWithResponses) GetEventWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetEventResponse, error) {
 	rsp, err := c.GetEvent(ctx, reqEditors...)
@@ -2825,6 +3045,32 @@ func ParsePostAppInitializeResponse(rsp *http.Response) (*PostAppInitializeRespo
 	return response, nil
 }
 
+// ParsePostConfigGetResponse parses an HTTP response from a PostConfigGetWithResponse call
+func ParsePostConfigGetResponse(rsp *http.Response) (*PostConfigGetResponse, error) {
+	bodyBytes, err := io.ReadAll(rsp.Body)
+	defer func() { _ = rsp.Body.Close() }()
+	if err != nil {
+		return nil, err
+	}
+
+	response := &PostConfigGetResponse{
+		Body:         bodyBytes,
+		HTTPResponse: rsp,
+	}
+
+	switch {
+	case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+		var dest ConfigInfo
+		if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+			return nil, err
+		}
+		response.JSON200 = &dest
+
+	}
+
+	return response, nil
+}
+
 // ParseGetEventResponse parses an HTTP response from a GetEventWithResponse call
 func ParseGetEventResponse(rsp *http.Response) (*GetEventResponse, error) {
 	bodyBytes, err := io.ReadAll(rsp.Body)