mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 18:33:04 +08:00
feat: add project profiles for snapshot-based config switching
Add a profile feature that captures the current provider, MCP, skills and prompt state for Claude Code and Codex as a named snapshot, and re-applies it in one click from the header switcher or the tray Projects submenu. - New profiles table (schema v12) with current marker in settings - ProfileService orchestrates the four existing switch primitives (provider first, then MCP diff, skills diff, prompt enable) - Best-effort apply: dangling references become warnings, no rollback - Header combobox switcher + snapshot-style manage dialog - Tray Projects submenu shared with the UI apply/event pipeline - i18n for zh/en/ja/zh-TW under the new profiles domain - Integration tests covering roundtrip, dangling refs and clear
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { profilesApi, providersApi } from "@/lib/api";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
|
||||
const updateTrayMenuSafely = async () => {
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (trayError) {
|
||||
console.error("Failed to update tray menu after profile change", trayError);
|
||||
}
|
||||
};
|
||||
|
||||
export const useProfilesQuery = () => {
|
||||
return useQuery({
|
||||
queryKey: ["profiles"],
|
||||
queryFn: () => profilesApi.list(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateProfileMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (name: string) => profilesApi.create(name),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["profiles"] });
|
||||
await updateTrayMenuSafely();
|
||||
toast.success(t("profiles.createSuccess"), { closeButton: true });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(t("profiles.createFailed", { detail }), {
|
||||
closeButton: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProfileMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
name,
|
||||
resnapshot,
|
||||
}: {
|
||||
id: string;
|
||||
name?: string;
|
||||
resnapshot?: boolean;
|
||||
}) => profilesApi.update(id, { name, resnapshot }),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["profiles"] });
|
||||
await updateTrayMenuSafely();
|
||||
toast.success(t("profiles.updateSuccess"), { closeButton: true });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(t("profiles.updateFailed", { detail }), {
|
||||
closeButton: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteProfileMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => profilesApi.delete(id),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["profiles"] });
|
||||
await updateTrayMenuSafely();
|
||||
toast.success(t("profiles.deleteSuccess"), { closeButton: true });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(t("profiles.deleteFailed", { detail }), {
|
||||
closeButton: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useClearProfileMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => profilesApi.clearCurrent(),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["profiles"] });
|
||||
await updateTrayMenuSafely();
|
||||
toast.success(t("profiles.clearSuccess"), { closeButton: true });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(t("profiles.applyFailed", { detail }), {
|
||||
closeButton: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useApplyProfileMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => profilesApi.apply(id),
|
||||
onSuccess: async (warnings) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["profiles"] });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["providers", "claude"],
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ["providers", "codex"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["mcp", "all"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["skills"] });
|
||||
await updateTrayMenuSafely();
|
||||
|
||||
if (warnings.length > 0) {
|
||||
toast.warning(
|
||||
t("profiles.applyWarnings", {
|
||||
warningCount: warnings.length,
|
||||
details: warnings.join("\n"),
|
||||
}),
|
||||
{ closeButton: true, duration: 10000 },
|
||||
);
|
||||
} else {
|
||||
toast.success(t("profiles.applySuccess"), { closeButton: true });
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(t("profiles.applyFailed", { detail }), {
|
||||
closeButton: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user