- feat(codex): Add “Apply to VS Code/Remove from VS Code” button on current Codex provider card

- feat(tauri): Add commands to read/write VS Code settings.json with cross-variant detection (Code/Insiders/VSCodium/OSS)
- fix(vscode): Use top-level keys “chatgpt.apiBase” and “chatgpt.config.preferred_auth_method”
- fix(vscode): Handle empty settings.json (skip deletes, direct write) to avoid “Can not delete in empty document”
- fix(windows): Make atomic writes robust by removing target before rename
- ui(provider-list): Improve error surfacing when applying/removing
- chore(types): Extend window.api typings and tauri-api wrappers for VS Code commands
- deps: Add jsonc-parser
This commit is contained in:
Jason
2025-09-19 08:30:29 +08:00
parent 04e81ebbe3
commit 3a9a8036d2
11 changed files with 391 additions and 2 deletions

View File

@@ -1,7 +1,9 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { Provider } from "../types";
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
import { AppType } from "../lib/tauri-api";
import { applyProviderToVSCode, detectApplied } from "../utils/vscodeSettings";
// 不再在列表中显示分类徽章,避免造成困惑
interface ProviderListProps {
@@ -10,6 +12,8 @@ interface ProviderListProps {
onSwitch: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string) => void;
appType?: AppType;
onNotify?: (message: string, type: "success" | "error", duration?: number) => void;
}
const ProviderList: React.FC<ProviderListProps> = ({
@@ -18,6 +22,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
onSwitch,
onDelete,
onEdit,
appType,
onNotify,
}) => {
// 提取API地址兼容不同供应商配置Claude env / Codex TOML
const getApiUrl = (provider: Provider): string => {
@@ -46,6 +52,102 @@ const ProviderList: React.FC<ProviderListProps> = ({
}
};
// 解析 Codex 配置中的 base_url仅用于 VS Code 写入)
const getCodexBaseUrl = (provider: Provider): string | undefined => {
try {
const cfg = provider.settingsConfig;
const text = typeof cfg?.config === "string" ? cfg.config : "";
if (!text) return undefined;
const m = text.match(/base_url\s*=\s*"([^"]+)"/);
return m && m[1] ? m[1] : undefined;
} catch {
return undefined;
}
};
// VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否“已应用”变化
const [vscodeAppliedFor, setVscodeAppliedFor] = useState<string | null>(null);
// 当当前供应商或 appType 变化时,尝试读取 VS Code settings 并检测状态
useEffect(() => {
const check = async () => {
if (appType !== "codex" || !currentProviderId) {
setVscodeAppliedFor(null);
return;
}
const status = await window.api.getVSCodeSettingsStatus();
if (!status.exists) {
setVscodeAppliedFor(null);
return;
}
try {
const content = await window.api.readVSCodeSettings();
const detected = detectApplied(content);
// 认为“已应用”的条件:存在任意一个我们管理的键
const applied = detected.hasApiBase || detected.hasPreferredAuthMethod;
setVscodeAppliedFor(applied ? currentProviderId : null);
} catch {
setVscodeAppliedFor(null);
}
};
check();
}, [appType, currentProviderId]);
const handleApplyToVSCode = async (provider: Provider) => {
try {
const status = await window.api.getVSCodeSettingsStatus();
if (!status.exists) {
onNotify?.("未找到 VS Code 用户设置文件 (settings.json)", "error", 3000);
return;
}
const raw = await window.api.readVSCodeSettings();
const isOfficial = provider.category === "official";
const baseUrl = isOfficial ? undefined : getCodexBaseUrl(provider);
const next = applyProviderToVSCode(raw, { baseUrl, isOfficial });
if (next === raw) {
// 幂等:没有变化也提示成功
onNotify?.("已应用到 VS Code", "success", 1500);
setVscodeAppliedFor(provider.id);
return;
}
await window.api.writeVSCodeSettings(next);
onNotify?.("已应用到 VS Code", "success", 1500);
setVscodeAppliedFor(provider.id);
} catch (e: any) {
console.error(e);
const msg = (e && e.message) ? e.message : "应用到 VS Code 失败";
onNotify?.(msg, "error", 5000);
}
};
const handleRemoveFromVSCode = async () => {
try {
const status = await window.api.getVSCodeSettingsStatus();
if (!status.exists) {
onNotify?.("未找到 VS Code 用户设置文件 (settings.json)", "error", 3000);
return;
}
const raw = await window.api.readVSCodeSettings();
const next = applyProviderToVSCode(raw, { baseUrl: undefined, isOfficial: true });
if (next === raw) {
onNotify?.("已从 VS Code 移除", "success", 1500);
setVscodeAppliedFor(null);
return;
}
await window.api.writeVSCodeSettings(next);
onNotify?.("已从 VS Code 移除", "success", 1500);
setVscodeAppliedFor(null);
} catch (e: any) {
console.error(e);
const msg = (e && e.message) ? e.message : "移除失败";
onNotify?.(msg, "error", 5000);
}
};
// 对供应商列表进行排序
const sortedProviders = Object.values(providers).sort((a, b) => {
// 按添加时间排序
@@ -133,6 +235,28 @@ const ProviderList: React.FC<ProviderListProps> = ({
</div>
<div className="flex items-center gap-2 ml-4">
{appType === "codex" && isCurrent && (
<button
onClick={() =>
vscodeAppliedFor === provider.id
? handleRemoveFromVSCode()
: handleApplyToVSCode(provider)
}
className={cn(
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
vscodeAppliedFor === provider.id
? "bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
: "bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700",
)}
title={
vscodeAppliedFor === provider.id
? "从 VS Code 移除我们写入的配置"
: "将当前供应商应用到 VS Code"
}
>
{vscodeAppliedFor === provider.id ? "从 VS Code 移除" : "应用到 VS Code"}
</button>
)}
<button
onClick={() => onSwitch(provider.id)}
disabled={isCurrent}