Files
CC-Switch/src/lib/query/profiles.ts
T
Jason 65a5464fcc feat(profiles): include Claude Desktop provider in project profiles
Claude Desktop's only dimension managed by cc-switch is its provider
(MCP/Skills are hardcoded unsupported and prompts have no live file),
so snapshots capture just the current desktop provider while the empty
MCP/Skills sets and None prompt make apply a natural no-op for the
other dimensions - no per-dimension special casing needed.

- Add claude-desktop slot to PROFILE_APPS and PerApp (serde key uses
  the hyphenated app id); old payloads without the key deserialize to
  None and leave Claude Desktop untouched on apply
- Gate the tray Projects submenu by iterating PROFILE_APPS instead of
  hardcoding Claude/Codex visibility
- Show the profile switcher on the Claude Desktop tab, mirror the
  PerApp type, invalidate the claude-desktop providers cache on apply,
  and extend the switcher tooltip in all four locales
- Extend the roundtrip integration test with the desktop provider
  dimension; the desktop switch is cfg-gated to macOS/Windows because
  desktop live writes error on Linux where CI runs cargo test
2026-07-07 10:49:32 +08:00

149 lines
4.5 KiB
TypeScript

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", "claude-desktop"],
});
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,
});
},
});
};