Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 64 additions & 29 deletions src/hooks/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,57 @@ export function useSettings(): UseSettingsResult {
setRequiresRestart,
]);

// 同步 Claude 插件集成配置到 ~/.claude/settings.json
// 返回 true 表示已执行过 syncCurrentProvidersLiveSafe,调用方可跳过重复同步
const syncClaudePluginIfChanged = useCallback(
async (enabled: boolean | undefined): Promise<boolean> => {
if (
enabled === undefined ||
enabled === data?.enableClaudePluginIntegration
)
return false;
try {
if (enabled) {
const currentId = await providersApi.getCurrent("claude");
let isOfficial = false;
if (currentId) {
const allProviders = await providersApi.getAll("claude");
isOfficial = allProviders[currentId]?.category === "official";
}
await settingsApi.applyClaudePluginConfig({ official: isOfficial });
} else {
await settingsApi.applyClaudePluginConfig({ official: true });
}

const syncResult = await syncCurrentProvidersLiveSafe();
if (!syncResult.ok) {
console.warn(
"[useSettings] Failed to sync providers after toggling Claude plugin",
syncResult.error,
);
toast.error(
t("notifications.syncClaudePluginFailed", {
defaultValue: "同步 Claude 插件失败",
}),
);
}
return true;
} catch (error) {
console.warn(
"[useSettings] Failed to sync Claude plugin config",
error,
);
toast.error(
t("notifications.syncClaudePluginFailed", {
defaultValue: "同步 Claude 插件失败",
}),
);
return false;
}
},
[data?.enableClaudePluginIntegration, t],
);

// 即时保存设置(用于 General 标签页的实时更新)
// 保存基础配置 + 独立的系统 API 调用(开机自启)
const autoSaveSettings = useCallback(
Expand Down Expand Up @@ -197,6 +248,8 @@ export function useSettings(): UseSettingsResult {
}
}

await syncClaudePluginIfChanged(payload.enableClaudePluginIntegration);

// 持久化语言偏好
try {
if (typeof window !== "undefined" && updates.language) {
Expand Down Expand Up @@ -228,7 +281,7 @@ export function useSettings(): UseSettingsResult {
throw error;
}
},
[data, saveMutation, settings, t],
[data, saveMutation, settings, syncClaudePluginIfChanged, t],
);

// 完整保存设置(用于 Advanced 标签页的手动保存)
Expand Down Expand Up @@ -313,30 +366,9 @@ export function useSettings(): UseSettingsResult {
}
}

// 只在 Claude 插件集成状态真正改变时调用系统 API
if (
payload.enableClaudePluginIntegration !== undefined &&
payload.enableClaudePluginIntegration !==
data?.enableClaudePluginIntegration
) {
try {
if (payload.enableClaudePluginIntegration) {
await settingsApi.applyClaudePluginConfig({ official: false });
} else {
await settingsApi.applyClaudePluginConfig({ official: true });
}
} catch (error) {
console.warn(
"[useSettings] Failed to sync Claude plugin config",
error,
);
toast.error(
t("notifications.syncClaudePluginFailed", {
defaultValue: "同步 Claude 插件失败",
}),
);
}
}
const pluginSynced = await syncClaudePluginIfChanged(
payload.enableClaudePluginIntegration,
);

try {
if (typeof window !== "undefined") {
Expand All @@ -359,15 +391,17 @@ export function useSettings(): UseSettingsResult {
}

// 如果 Claude/Codex/Gemini/OpenCode 的目录覆盖发生变化,则立即将"当前使用的供应商"写回对应应用的 live 配置
// 如果插件同步已经执行过 syncCurrentProvidersLiveSafe,则跳过避免重复
const claudeDirChanged = sanitizedClaudeDir !== previousClaudeDir;
const codexDirChanged = sanitizedCodexDir !== previousCodexDir;
const geminiDirChanged = sanitizedGeminiDir !== previousGeminiDir;
const opencodeDirChanged = sanitizedOpencodeDir !== previousOpencodeDir;
if (
claudeDirChanged ||
codexDirChanged ||
geminiDirChanged ||
opencodeDirChanged
!pluginSynced &&
(claudeDirChanged ||
codexDirChanged ||
geminiDirChanged ||
opencodeDirChanged)
) {
const syncResult = await syncCurrentProvidersLiveSafe();
if (!syncResult.ok) {
Expand Down Expand Up @@ -409,6 +443,7 @@ export function useSettings(): UseSettingsResult {
saveMutation,
settings,
setRequiresRestart,
syncClaudePluginIfChanged,
t,
],
);
Expand Down
11 changes: 10 additions & 1 deletion tests/hooks/useSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const applyClaudeOnboardingSkipMock = vi.fn();
const clearClaudeOnboardingSkipMock = vi.fn();
const syncCurrentProvidersLiveMock = vi.fn();
const updateTrayMenuMock = vi.fn();
const getCurrentMock = vi.fn();
const getAllMock = vi.fn();
const toastErrorMock = vi.fn();
const toastSuccessMock = vi.fn();

Expand Down Expand Up @@ -61,6 +63,8 @@ vi.mock("@/lib/api", () => ({
},
providersApi: {
updateTrayMenu: (...args: unknown[]) => updateTrayMenuMock(...args),
getCurrent: (...args: unknown[]) => getCurrentMock(...args),
getAll: (...args: unknown[]) => getAllMock(...args),
},
}));

Expand Down Expand Up @@ -121,6 +125,8 @@ describe("useSettings hook", () => {
applyClaudeOnboardingSkipMock.mockReset();
clearClaudeOnboardingSkipMock.mockReset();
syncCurrentProvidersLiveMock.mockReset();
getCurrentMock.mockReset();
getAllMock.mockReset();
toastErrorMock.mockReset();
toastSuccessMock.mockReset();
window.localStorage.clear();
Expand Down Expand Up @@ -154,6 +160,9 @@ describe("useSettings hook", () => {
applyClaudePluginConfigMock.mockResolvedValue(true);
applyClaudeOnboardingSkipMock.mockResolvedValue(true);
clearClaudeOnboardingSkipMock.mockResolvedValue(true);
syncCurrentProvidersLiveMock.mockResolvedValue({ ok: true });
getCurrentMock.mockResolvedValue(null);
getAllMock.mockResolvedValue({});
});

it("auto-saves and applies Claude onboarding skip when toggled on", async () => {
Expand Down Expand Up @@ -262,7 +271,7 @@ describe("useSettings hook", () => {
expect(metadataMock.setRequiresRestart).toHaveBeenCalledWith(true);
expect(window.localStorage.getItem("language")).toBe("en");
expect(toastErrorMock).not.toHaveBeenCalled();
// 目录有变化,应触发一次同步当前供应商到 live
// 插件同步已包含 syncCurrentProvidersLiveSafe,目录变更不再重复调用
expect(syncCurrentProvidersLiveMock).toHaveBeenCalledTimes(1);
});

Expand Down