From 45dbb1a38d4ca62e1f3bceafd738f54427a05a7e Mon Sep 17 00:00:00 2001 From: Roderick Ford Date: Thu, 27 Nov 2025 20:32:49 -0700 Subject: [PATCH] feat: add config reload command to restart MCP servers without restart - Add POST /config/reload server endpoint that disposes cached state and re-reads opencode.json - Add 'Reload config (opencode.json)' command to TUI command palette (Ctrl+P) - Expose baseUrl from SDK context for direct API calls - Add config.reload command type and ConfigReloaded event This allows users to modify opencode.json (e.g., add/change MCP servers) and reload the configuration without restarting opencode entirely. --- packages/opencode/src/cli/cmd/tui/app.tsx | 59 ++++++++++++++++++- .../opencode/src/cli/cmd/tui/context/sdk.tsx | 2 +- packages/opencode/src/cli/cmd/tui/event.ts | 8 +++ packages/opencode/src/server/server.ts | 46 +++++++++++++++ 4 files changed, 112 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index c382a0f1494..13eabadb44a 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -7,7 +7,7 @@ import { Installation } from "@/installation" import { Global } from "@/global" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { SDKProvider, useSDK } from "@tui/context/sdk" -import { SyncProvider } from "@tui/context/sync" +import { SyncProvider, useSync } from "@tui/context/sync" import { LocalProvider, useLocal } from "@tui/context/local" import { DialogModel } from "@tui/component/dialog-model" import { DialogStatus } from "@tui/component/dialog-status" @@ -171,7 +171,9 @@ function App() { const local = useLocal() const kv = useKV() const command = useCommandDialog() - const { event } = useSDK() + const sdk = useSDK() + const { event } = sdk + const sync = useSync() const toast = useToast() const { theme, mode, setMode } = useTheme() const exit = useExit() @@ -291,6 +293,59 @@ function App() { }, category: "System", }, + { + title: "Reload config (opencode.json)", + value: "config.reload", + category: "System", + onSelect: async () => { + dialog.clear() + toast.show({ + variant: "info", + message: "Reloading config...", + duration: 2000, + }) + try { + // Call the reload endpoint directly since SDK may not have it yet + const baseUrl = sdk.baseUrl + const directory = encodeURIComponent(process.cwd()) + const response = await fetch(`${baseUrl}/config/reload?directory=${directory}`, { + method: "POST", + }) + const result = await response.json() + + if (result.success) { + // Refresh sync data after config reload + const [providers, agents, config, mcpStatus] = await Promise.all([ + sdk.client.config.providers(), + sdk.client.app.agents(), + sdk.client.config.get(), + sdk.client.mcp.status(), + ]) + sync.set("provider", providers.data!.providers) + sync.set("agent", agents.data ?? []) + sync.set("config", config.data!) + sync.set("mcp", mcpStatus.data!) + toast.show({ + variant: "success", + message: result.message || "Config reloaded successfully", + duration: 3000, + }) + } else { + toast.show({ + variant: "error", + message: result.message || "Failed to reload config", + duration: 5000, + }) + } + } catch (error) { + toast.show({ + variant: "error", + message: `Reload failed: ${error instanceof Error ? error.message : String(error)}`, + duration: 5000, + }) + } + }, + }, { title: "Exit the app", value: "app.exit", diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 655c6802248..6c196ea4627 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -32,6 +32,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ abort.abort() }) - return { client: sdk, event: emitter } + return { client: sdk, event: emitter, baseUrl: props.url } }, }) diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index 005d0d54aef..9412d3a23a0 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -22,6 +22,7 @@ export const TuiEvent = { "prompt.clear", "prompt.submit", "agent.cycle", + "config.reload", ]), z.string(), ]), @@ -36,4 +37,11 @@ export const TuiEvent = { duration: z.number().default(5000).optional().describe("Duration in milliseconds"), }), ), + ConfigReloaded: Bus.event( + "tui.config.reloaded", + z.object({ + success: z.boolean(), + message: z.string().optional(), + }), + ), } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 59e066e15c2..a84b0d8871c 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -192,6 +192,52 @@ export namespace Server { return c.json(config) }, ) + .post( + "/config/reload", + describeRoute({ + description: "Reload config from disk (including MCP servers)", + operationId: "config.reload", + responses: { + 200: { + description: "Config reloaded successfully", + content: { + "application/json": { + schema: resolver( + z.object({ + success: z.boolean(), + message: z.string().optional(), + config: Config.Info.optional(), + }).meta({ ref: "ConfigReloadResult" }) + ), + }, + }, + }, + ...errors(400), + }, + }), + async (c) => { + try { + // Dispose current instance state (clears config cache, MCP connections, etc.) + await Instance.dispose() + // Re-fetch config (will re-read opencode.json and restart MCP servers) + const config = await Config.get() + // Trigger MCP reconnection by accessing MCP status + const mcpStatus = await MCP.status() + log.info("config reloaded", { mcpServers: Object.keys(mcpStatus).length }) + return c.json({ + success: true, + message: `Config reloaded. ${Object.keys(mcpStatus).length} MCP server(s) configured.`, + config, + }) + } catch (error) { + log.error("config reload failed", { error }) + return c.json({ + success: false, + message: error instanceof Error ? error.message : String(error), + }) + } + }, + ) .get( "/experimental/tool/ids", describeRoute({