import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Check, ChevronsUpDown, FolderCog, FolderOpen, Plus, X, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import type { AppId } from "@/lib/api/types"; import { useApplyProfileMutation, useClearProfileMutation, useCreateProfileMutation, useProfilesQuery, } from "@/lib/query/profiles"; import { ProfileManageDialog } from "./ProfileManageDialog"; import { APP_PROFILE_SCOPE, hasScopeSnapshot } from "./scope"; import type { CurrentProfileIds, ProfileScope } from "@/lib/api/profiles"; const CURRENT_ID_KEY: Record = { claude: "claude", "claude-desktop": "claudeDesktop", codex: "codex", }; interface ProfileSwitcherProps { activeApp: AppId; } /** * 项目 Profile 切换器(header 左侧入口) * * 项目列表全应用共享(用户拥有的项目就那几个),但切换按分组进行: * Claude 组(Claude Code 的供应商/MCP/Skills/记忆文件 + Claude Desktop * 的供应商)与 Codex 组各自指向自己的当前项目、只应用组内快照。 * 与右侧 AppSwitcher(仅切换查看的应用)语义不同。 */ export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [isCreateOpen, setIsCreateOpen] = useState(false); const [isManageOpen, setIsManageOpen] = useState(false); const [newName, setNewName] = useState(""); const { data } = useProfilesQuery(); const applyMutation = useApplyProfileMutation(); const clearMutation = useClearProfileMutation(); const createMutation = useCreateProfileMutation(); // Profile 仅作用于受支持的应用——在其他应用的标签页展示会误导用户 // 以为当前应用也被切换了,因此只在有所属分组的应用页面渲染 const scope = APP_PROFILE_SCOPE[activeApp]; if (!scope) { return null; } const profiles = data?.profiles ?? []; const currentId = data?.currentIds?.[CURRENT_ID_KEY[scope]] ?? null; const currentProfile = profiles.find((p) => p.id === currentId); const handleApply = (id: string) => { setOpen(false); if (id !== currentId) { applyMutation.mutate({ id, scope }); } }; const closeCreateDialog = () => { setIsCreateOpen(false); setNewName(""); }; const handleCreate = () => { const name = newName.trim(); if (!name) return; createMutation.mutate({ name, scope }, { onSuccess: closeCreateDialog }); }; return ( <> {t("profiles.empty")} {profiles.length > 0 && ( {profiles.map((profile) => ( handleApply(profile.id)} > {profile.name} {!hasScopeSnapshot(profile, scope) && ( {t("profiles.noSnapshotForScope")} )} ))} )}
{ setOpen(false); setIsCreateOpen(true); }} > {t("profiles.createFromCurrent")} {currentId && ( { setOpen(false); clearMutation.mutate(scope); }} > {t("profiles.none")} )} {profiles.length > 0 && ( { setOpen(false); setIsManageOpen(true); }} > {t("profiles.manage")} )} { if (!open) closeCreateDialog(); }} > {t("profiles.createFromCurrent")} {t(`profiles.createDescription.${scope}`)}
setNewName(e.target.value)} placeholder={t("profiles.namePlaceholder")} autoFocus onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); }} />
setIsManageOpen(false)} /> ); }