feat(opencode): Phase 7 - Frontend TypeScript type definitions

- Add "opencode" to AppId type in lib/api/types.ts
- Extend McpApps interface with opencode field in types.ts
- Add OpenCode-specific types: OpenCodeModel, OpenCodeProviderOptions,
  OpenCodeProviderConfig, OpenCodeMcpServerSpec
- Add opencodeConfigDir to Settings interface
- Add importOpenCodeFromLive() to providersApi
- Fix type errors across components:
  - AppSwitcher: add opencode to icon/name mappings
  - McpFormModal: add opencode to enabledApps state
  - PromptFormModal/Panel: add opencode filename mapping
  - EndpointSpeedTest: add opencode timeout config
  - useBaseUrlState: add opencode to appType union
  - ProxyToggle: add opencode label
  - App.tsx: handle opencode fallback for SkillsPage
- Update ProxyTakeoverStatus with opencode field (always false)
- Fix test mocks in tests/msw/state.ts
This commit is contained in:
Jason
2026-01-15 16:25:14 +08:00
parent 7997b2c7b3
commit 21754a7349
13 changed files with 78 additions and 11 deletions
+1 -1
View File
@@ -454,7 +454,7 @@ function App() {
/>
);
case "skillsDiscovery":
return <SkillsPage ref={skillsPageRef} initialApp={activeApp} />;
return <SkillsPage ref={skillsPageRef} initialApp={activeApp === "opencode" ? "claude" : activeApp} />;
case "mcp":
return (
<UnifiedMcpPanel
+2
View File
@@ -16,11 +16,13 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
claude: "claude",
codex: "openai",
gemini: "gemini",
opencode: "opencode",
};
const appDisplayName: Record<AppId, string> = {
claude: "Claude",
codex: "Codex",
gemini: "Gemini",
opencode: "OpenCode",
};
return (
+2
View File
@@ -65,6 +65,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
claude: boolean;
codex: boolean;
gemini: boolean;
opencode: boolean;
}>(() => {
if (initialData?.apps) {
return { ...initialData.apps };
@@ -73,6 +74,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
claude: defaultEnabledApps.includes("claude"),
codex: defaultEnabledApps.includes("codex"),
gemini: defaultEnabledApps.includes("gemini"),
opencode: defaultEnabledApps.includes("opencode"),
};
});
@@ -34,6 +34,7 @@ const PromptFormModal: React.FC<PromptFormModalProps> = ({
claude: "CLAUDE.md",
codex: "AGENTS.md",
gemini: "GEMINI.md",
opencode: "OPENCODE.md",
};
const filename = filenameMap[appId];
const [name, setName] = useState("");
@@ -28,6 +28,7 @@ const PromptFormPanel: React.FC<PromptFormPanelProps> = ({
claude: "CLAUDE.md",
codex: "AGENTS.md",
gemini: "GEMINI.md",
opencode: "OPENCODE.md",
};
const filename = filenameMap[appId];
const [name, setName] = useState("");
@@ -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<AppId, number> = {
codex: 12,
claude: 8,
gemini: 8, // 新增 gemini
} as const;
gemini: 8,
opencode: 8,
};
interface TestResult {
url: string;
@@ -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;
+3 -1
View File
@@ -37,7 +37,9 @@ export function ProxyToggle({ className, activeApp }: ProxyToggleProps) {
? "Claude"
: activeApp === "codex"
? "Codex"
: "Gemini";
: activeApp === "gemini"
? "Gemini"
: "OpenCode";
const tooltipText = takeoverEnabled
? isRunning
+8
View File
@@ -74,6 +74,14 @@ export const providersApi = {
async openTerminal(providerId: string, appId: AppId): Promise<boolean> {
return await invoke("open_provider_terminal", { providerId, app: appId });
},
/**
* 从 OpenCode live 配置导入供应商到数据库
* OpenCode 特有功能:由于累加模式,用户可能已在 opencode.json 中配置供应商
*/
async importOpenCodeFromLive(): Promise<number> {
return await invoke("import_opencode_providers_from_live");
},
};
// ============================================================================
+1 -1
View File
@@ -1,2 +1,2 @@
// 前端统一使用 AppId 作为应用标识(与后端命令参数 `app` 一致)
export type AppId = "claude" | "codex" | "gemini"; // 新增 gemini
export type AppId = "claude" | "codex" | "gemini" | "opencode";
+45
View File
@@ -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<string, UniversalProvider>;
// ============================================================================
// 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<string, string>;
}
// OpenCode 供应商配置(settings_config 结构)
export interface OpenCodeProviderConfig {
npm: string; // AI SDK 包名,如 "@ai-sdk/openai-compatible"
name?: string; // 供应商显示名称
options: OpenCodeProviderOptions;
models: Record<string, OpenCodeModel>;
}
// OpenCode MCP 服务器配置(与统一格式不同)
export interface OpenCodeMcpServerSpec {
type: "local" | "remote";
// local 类型字段
command?: string[]; // 与统一格式不同:命令和参数合并为数组
environment?: Record<string, string>; // 与统一格式不同:使用 environment 而非 env
// remote 类型字段
url?: string;
headers?: Record<string, string>;
// 通用字段
enabled?: boolean;
}
+1
View File
@@ -45,6 +45,7 @@ export interface ProxyTakeoverStatus {
claude: boolean;
codex: boolean;
gemini: boolean;
opencode: boolean;
}
export interface ProviderHealth {
+8 -4
View File
@@ -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: {},
};
};