diff --git a/src/App.tsx b/src/App.tsx
index 452afed5..32673da6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -454,7 +454,7 @@ function App() {
/>
);
case "skillsDiscovery":
- return ;
+ return ;
case "mcp":
return (
= {
claude: "Claude",
codex: "Codex",
gemini: "Gemini",
+ opencode: "OpenCode",
};
return (
diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx
index 5c608a4f..5904e49d 100644
--- a/src/components/mcp/McpFormModal.tsx
+++ b/src/components/mcp/McpFormModal.tsx
@@ -65,6 +65,7 @@ const McpFormModal: React.FC = ({
claude: boolean;
codex: boolean;
gemini: boolean;
+ opencode: boolean;
}>(() => {
if (initialData?.apps) {
return { ...initialData.apps };
@@ -73,6 +74,7 @@ const McpFormModal: React.FC = ({
claude: defaultEnabledApps.includes("claude"),
codex: defaultEnabledApps.includes("codex"),
gemini: defaultEnabledApps.includes("gemini"),
+ opencode: defaultEnabledApps.includes("opencode"),
};
});
diff --git a/src/components/prompts/PromptFormModal.tsx b/src/components/prompts/PromptFormModal.tsx
index 194df686..8eb043e7 100644
--- a/src/components/prompts/PromptFormModal.tsx
+++ b/src/components/prompts/PromptFormModal.tsx
@@ -34,6 +34,7 @@ const PromptFormModal: React.FC = ({
claude: "CLAUDE.md",
codex: "AGENTS.md",
gemini: "GEMINI.md",
+ opencode: "OPENCODE.md",
};
const filename = filenameMap[appId];
const [name, setName] = useState("");
diff --git a/src/components/prompts/PromptFormPanel.tsx b/src/components/prompts/PromptFormPanel.tsx
index bdd9efbe..7f2f884c 100644
--- a/src/components/prompts/PromptFormPanel.tsx
+++ b/src/components/prompts/PromptFormPanel.tsx
@@ -28,6 +28,7 @@ const PromptFormPanel: React.FC = ({
claude: "CLAUDE.md",
codex: "AGENTS.md",
gemini: "GEMINI.md",
+ opencode: "OPENCODE.md",
};
const filename = filenameMap[appId];
const [name, setName] = useState("");
diff --git a/src/components/providers/forms/EndpointSpeedTest.tsx b/src/components/providers/forms/EndpointSpeedTest.tsx
index 011116dd..5c217251 100644
--- a/src/components/providers/forms/EndpointSpeedTest.tsx
+++ b/src/components/providers/forms/EndpointSpeedTest.tsx
@@ -9,11 +9,12 @@ import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import type { CustomEndpoint, EndpointCandidate } from "@/types";
// 端点测速超时配置(秒)
-const ENDPOINT_TIMEOUT_SECS = {
+const ENDPOINT_TIMEOUT_SECS: Record = {
codex: 12,
claude: 8,
- gemini: 8, // 新增 gemini
-} as const;
+ gemini: 8,
+ opencode: 8,
+};
interface TestResult {
url: string;
diff --git a/src/components/providers/forms/hooks/useBaseUrlState.ts b/src/components/providers/forms/hooks/useBaseUrlState.ts
index 2deffcba..75e39594 100644
--- a/src/components/providers/forms/hooks/useBaseUrlState.ts
+++ b/src/components/providers/forms/hooks/useBaseUrlState.ts
@@ -6,7 +6,7 @@ import {
import type { ProviderCategory } from "@/types";
interface UseBaseUrlStateProps {
- appType: "claude" | "codex" | "gemini";
+ appType: "claude" | "codex" | "gemini" | "opencode";
category: ProviderCategory | undefined;
settingsConfig: string;
codexConfig?: string;
diff --git a/src/components/proxy/ProxyToggle.tsx b/src/components/proxy/ProxyToggle.tsx
index 2b9b8cb9..ca526eb4 100644
--- a/src/components/proxy/ProxyToggle.tsx
+++ b/src/components/proxy/ProxyToggle.tsx
@@ -37,7 +37,9 @@ export function ProxyToggle({ className, activeApp }: ProxyToggleProps) {
? "Claude"
: activeApp === "codex"
? "Codex"
- : "Gemini";
+ : activeApp === "gemini"
+ ? "Gemini"
+ : "OpenCode";
const tooltipText = takeoverEnabled
? isRunning
diff --git a/src/lib/api/providers.ts b/src/lib/api/providers.ts
index eb3dc9da..3e9d371b 100644
--- a/src/lib/api/providers.ts
+++ b/src/lib/api/providers.ts
@@ -74,6 +74,14 @@ export const providersApi = {
async openTerminal(providerId: string, appId: AppId): Promise {
return await invoke("open_provider_terminal", { providerId, app: appId });
},
+
+ /**
+ * 从 OpenCode live 配置导入供应商到数据库
+ * OpenCode 特有功能:由于累加模式,用户可能已在 opencode.json 中配置供应商
+ */
+ async importOpenCodeFromLive(): Promise {
+ return await invoke("import_opencode_providers_from_live");
+ },
};
// ============================================================================
diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts
index a51390e4..2e8e2b2a 100644
--- a/src/lib/api/types.ts
+++ b/src/lib/api/types.ts
@@ -1,2 +1,2 @@
// 前端统一使用 AppId 作为应用标识(与后端命令参数 `app` 一致)
-export type AppId = "claude" | "codex" | "gemini"; // 新增 gemini
+export type AppId = "claude" | "codex" | "gemini" | "opencode";
diff --git a/src/types.ts b/src/types.ts
index a9234d0d..5980eaed 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -125,6 +125,8 @@ export interface Settings {
codexConfigDir?: string;
// 覆盖 Gemini 配置目录(可选)
geminiConfigDir?: string;
+ // 覆盖 OpenCode 配置目录(可选)
+ opencodeConfigDir?: string;
// ===== 当前供应商 ID(设备级)=====
// 当前 Claude 供应商 ID(优先于数据库 is_current)
@@ -156,6 +158,7 @@ export interface McpApps {
claude: boolean;
codex: boolean;
gemini: boolean;
+ opencode: boolean;
}
// MCP 服务器条目(v3.7.0 统一结构)
@@ -247,3 +250,45 @@ export interface UniversalProvider {
// 统一供应商映射(id -> UniversalProvider)
export type UniversalProvidersMap = Record;
+
+// ============================================================================
+// OpenCode 专属配置(v3.9.2+)
+// ============================================================================
+
+// OpenCode 模型配置
+export interface OpenCodeModel {
+ name: string;
+ limit?: {
+ context?: number;
+ output?: number;
+ };
+}
+
+// OpenCode 供应商选项
+export interface OpenCodeProviderOptions {
+ baseURL?: string;
+ apiKey?: string;
+ headers?: Record;
+}
+
+// OpenCode 供应商配置(settings_config 结构)
+export interface OpenCodeProviderConfig {
+ npm: string; // AI SDK 包名,如 "@ai-sdk/openai-compatible"
+ name?: string; // 供应商显示名称
+ options: OpenCodeProviderOptions;
+ models: Record;
+}
+
+// OpenCode MCP 服务器配置(与统一格式不同)
+export interface OpenCodeMcpServerSpec {
+ type: "local" | "remote";
+ // local 类型字段
+ command?: string[]; // 与统一格式不同:命令和参数合并为数组
+ environment?: Record; // 与统一格式不同:使用 environment 而非 env
+ // remote 类型字段
+ url?: string;
+ headers?: Record;
+ // 通用字段
+ enabled?: boolean;
+}
+
diff --git a/src/types/proxy.ts b/src/types/proxy.ts
index 4d11370a..705c2288 100644
--- a/src/types/proxy.ts
+++ b/src/types/proxy.ts
@@ -45,6 +45,7 @@ export interface ProxyTakeoverStatus {
claude: boolean;
codex: boolean;
gemini: boolean;
+ opencode: boolean;
}
export interface ProviderHealth {
diff --git a/tests/msw/state.ts b/tests/msw/state.ts
index b5922a3a..ac385a67 100644
--- a/tests/msw/state.ts
+++ b/tests/msw/state.ts
@@ -57,12 +57,14 @@ const createDefaultProviders = (): ProvidersByApp => ({
createdAt: Date.now(),
},
},
+ opencode: {},
});
const createDefaultCurrent = (): CurrentProviderState => ({
claude: "claude-1",
codex: "codex-1",
gemini: "gemini-1",
+ opencode: "",
});
let providers = createDefaultProviders();
@@ -82,7 +84,7 @@ let mcpConfigs: McpConfigState = {
id: "sample",
name: "Sample Claude Server",
enabled: true,
- apps: { claude: true, codex: false, gemini: false },
+ apps: { claude: true, codex: false, gemini: false, opencode: false },
server: {
type: "stdio",
command: "claude-server",
@@ -94,7 +96,7 @@ let mcpConfigs: McpConfigState = {
id: "httpServer",
name: "HTTP Codex Server",
enabled: false,
- apps: { claude: false, codex: true, gemini: false },
+ apps: { claude: false, codex: true, gemini: false, opencode: false },
server: {
type: "http",
url: "http://localhost:3000",
@@ -102,6 +104,7 @@ let mcpConfigs: McpConfigState = {
},
},
gemini: {},
+ opencode: {},
};
const cloneProviders = (value: ProvidersByApp) =>
@@ -125,7 +128,7 @@ export const resetProviderState = () => {
id: "sample",
name: "Sample Claude Server",
enabled: true,
- apps: { claude: true, codex: false, gemini: false },
+ apps: { claude: true, codex: false, gemini: false, opencode: false },
server: {
type: "stdio",
command: "claude-server",
@@ -137,7 +140,7 @@ export const resetProviderState = () => {
id: "httpServer",
name: "HTTP Codex Server",
enabled: false,
- apps: { claude: false, codex: true, gemini: false },
+ apps: { claude: false, codex: true, gemini: false, opencode: false },
server: {
type: "http",
url: "http://localhost:3000",
@@ -145,6 +148,7 @@ export const resetProviderState = () => {
},
},
gemini: {},
+ opencode: {},
};
};