mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
refactor(profiles): shared project entity with per-scope switching
Projects are now global shared entities; Claude and Codex groups switch independently via scoped current pointers and scoped payload slots. - Remove scope column from profiles; keep current_profile_id_<scope> - Use Option<Vec<String>> for mcp/skills to distinguish 'never captured' from 'captured empty', preventing cross-side accidental disable - Update/apply operations scoped to the active group via merge_scope_from - Tray menu nests same shared list under Claude Code/Codex groups - Add i18n for per-scope tooltips and 'not saved for this side' hint Refs: profile P1 shared-entity redesign
This commit is contained in:
@@ -16,9 +16,12 @@ import {
|
||||
useProfilesQuery,
|
||||
useUpdateProfileMutation,
|
||||
} from "@/lib/query/profiles";
|
||||
import type { ProfileScope } from "@/lib/api/profiles";
|
||||
import { PROFILE_SCOPE_LABELS } from "./scope";
|
||||
|
||||
interface ProfileManageDialogProps {
|
||||
isOpen: boolean;
|
||||
scope: ProfileScope;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -31,11 +34,12 @@ type PendingConfirm = {
|
||||
/**
|
||||
* 项目管理对话框(纯快照式)
|
||||
*
|
||||
* 只提供 重命名 / 以当前状态更新快照 / 删除 三个操作;
|
||||
* 修改项目内容 = 先手动调好配置,再"以当前状态更新快照"。
|
||||
* 项目列表全应用共享,重命名/删除作用于共享实体;
|
||||
* "以当前状态更新快照"只覆盖发起页所属分组(scope)的槽位。
|
||||
*/
|
||||
export function ProfileManageDialog({
|
||||
isOpen,
|
||||
scope,
|
||||
onClose,
|
||||
}: ProfileManageDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -70,7 +74,7 @@ export function ProfileManageDialog({
|
||||
if (confirm.type === "delete") {
|
||||
deleteMutation.mutate(confirm.id);
|
||||
} else {
|
||||
updateMutation.mutate({ id: confirm.id, resnapshot: true });
|
||||
updateMutation.mutate({ id: confirm.id, resnapshot: true, scope });
|
||||
}
|
||||
setConfirm(null);
|
||||
};
|
||||
@@ -199,7 +203,10 @@ export function ProfileManageDialog({
|
||||
message={
|
||||
confirm.type === "delete"
|
||||
? t("profiles.deleteConfirmMessage", { name: confirm.name })
|
||||
: t("profiles.updateSnapshotConfirm", { name: confirm.name })
|
||||
: t("profiles.updateSnapshotConfirm", {
|
||||
name: confirm.name,
|
||||
group: PROFILE_SCOPE_LABELS[scope],
|
||||
})
|
||||
}
|
||||
variant={confirm.type === "delete" ? "destructive" : "info"}
|
||||
onConfirm={handleConfirm}
|
||||
|
||||
@@ -40,9 +40,7 @@ import {
|
||||
useProfilesQuery,
|
||||
} from "@/lib/query/profiles";
|
||||
import { ProfileManageDialog } from "./ProfileManageDialog";
|
||||
|
||||
/** 后端 services/profile.rs 的 PROFILE_APPS 前端镜像,扩展支持范围时两处同步 */
|
||||
const PROFILE_SUPPORTED_APPS: AppId[] = ["claude", "claude-desktop", "codex"];
|
||||
import { APP_PROFILE_SCOPE, hasScopeSnapshot } from "./scope";
|
||||
|
||||
interface ProfileSwitcherProps {
|
||||
activeApp: AppId;
|
||||
@@ -51,8 +49,10 @@ interface ProfileSwitcherProps {
|
||||
/**
|
||||
* 项目 Profile 切换器(header 左侧入口)
|
||||
*
|
||||
* Profile 是跨应用的配置快照(Claude Code + Codex 的供应商/MCP/Skills/记忆文件,
|
||||
* 以及 Claude Desktop 的供应商),与右侧 AppSwitcher(仅切换查看的应用)语义不同。
|
||||
* 项目列表全应用共享(用户拥有的项目就那几个),但切换按分组进行:
|
||||
* Claude 组(Claude Code 的供应商/MCP/Skills/记忆文件 + Claude Desktop
|
||||
* 的供应商)与 Codex 组各自指向自己的当前项目、只应用组内快照。
|
||||
* 与右侧 AppSwitcher(仅切换查看的应用)语义不同。
|
||||
*/
|
||||
export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -67,19 +67,20 @@ export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) {
|
||||
const createMutation = useCreateProfileMutation();
|
||||
|
||||
// Profile 仅作用于受支持的应用——在其他应用的标签页展示会误导用户
|
||||
// 以为当前应用也被切换了,因此只在受支持应用的页面渲染
|
||||
if (!PROFILE_SUPPORTED_APPS.includes(activeApp)) {
|
||||
// 以为当前应用也被切换了,因此只在有所属分组的应用页面渲染
|
||||
const scope = APP_PROFILE_SCOPE[activeApp];
|
||||
if (!scope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profiles = data?.profiles ?? [];
|
||||
const currentId = data?.currentId ?? null;
|
||||
const currentId = data?.currentIds?.[scope] ?? null;
|
||||
const currentProfile = profiles.find((p) => p.id === currentId);
|
||||
|
||||
const handleApply = (id: string) => {
|
||||
setOpen(false);
|
||||
if (id !== currentId) {
|
||||
applyMutation.mutate(id);
|
||||
applyMutation.mutate({ id, scope });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,7 +92,7 @@ export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) {
|
||||
const handleCreate = () => {
|
||||
const name = newName.trim();
|
||||
if (!name) return;
|
||||
createMutation.mutate(name, { onSuccess: closeCreateDialog });
|
||||
createMutation.mutate({ name, scope }, { onSuccess: closeCreateDialog });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -102,7 +103,7 @@ export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) {
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
title={t("profiles.switcherTooltip")}
|
||||
title={t(`profiles.switcherTooltip.${scope}`)}
|
||||
className={cn(
|
||||
"inline-flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors",
|
||||
"hover:bg-black/5 dark:hover:bg-white/5",
|
||||
@@ -144,6 +145,11 @@ export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) {
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{profile.name}</span>
|
||||
{!hasScopeSnapshot(profile, scope) && (
|
||||
<span className="ml-auto shrink-0 pl-2 text-xs text-muted-foreground">
|
||||
{t("profiles.noSnapshotForScope")}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
@@ -167,7 +173,7 @@ export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) {
|
||||
keywords={[t("profiles.none")]}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
clearMutation.mutate();
|
||||
clearMutation.mutate(scope);
|
||||
}}
|
||||
>
|
||||
<X className="mr-2 h-4 w-4 shrink-0" />
|
||||
@@ -203,7 +209,7 @@ export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) {
|
||||
<DialogHeader className="space-y-3 border-b-0 bg-transparent pb-0">
|
||||
<DialogTitle>{t("profiles.createFromCurrent")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("profiles.createDescription")}
|
||||
{t(`profiles.createDescription.${scope}`)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="px-6 pt-3">
|
||||
@@ -233,6 +239,7 @@ export function ProfileSwitcher({ activeApp }: ProfileSwitcherProps) {
|
||||
|
||||
<ProfileManageDialog
|
||||
isOpen={isManageOpen}
|
||||
scope={scope}
|
||||
onClose={() => setIsManageOpen(false)}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { AppId } from "@/lib/api/types";
|
||||
import type { PerApp, Profile, ProfileScope } from "@/lib/api/profiles";
|
||||
|
||||
/**
|
||||
* 应用页 → 所属项目分组(后端 ProfileScope::for_app 的前端镜像,两处同步)
|
||||
*
|
||||
* 不在映射里的应用不支持 Profile,其标签页不渲染切换器。
|
||||
*/
|
||||
export const APP_PROFILE_SCOPE: Partial<Record<AppId, ProfileScope>> = {
|
||||
claude: "claude",
|
||||
"claude-desktop": "claude",
|
||||
codex: "codex",
|
||||
};
|
||||
|
||||
/** 分组显示名(产品名,不进 i18n;与后端托盘子菜单标签一致) */
|
||||
export const PROFILE_SCOPE_LABELS: Record<ProfileScope, string> = {
|
||||
claude: "Claude Code",
|
||||
codex: "Codex",
|
||||
};
|
||||
|
||||
/** 分组内的 payload 槽位 key(后端 ProfileScope::apps 的前端镜像) */
|
||||
const SCOPE_SLOT_KEYS: Record<ProfileScope, (keyof PerApp<unknown>)[]> = {
|
||||
claude: ["claude", "claude-desktop"],
|
||||
codex: ["codex"],
|
||||
};
|
||||
|
||||
/**
|
||||
* 项目在某分组是否拍过快照(任一槽位非 null 即视为拍过)
|
||||
*
|
||||
* 未拍过的项目在该分组应用时不改动配置,只绑定 current 标记。
|
||||
*/
|
||||
export function hasScopeSnapshot(profile: Profile, scope: ProfileScope) {
|
||||
const { providers, mcp, skills, prompts } = profile.payload;
|
||||
return SCOPE_SLOT_KEYS[scope].some(
|
||||
(app) =>
|
||||
providers[app] !== null ||
|
||||
mcp[app] !== null ||
|
||||
skills[app] !== null ||
|
||||
prompts[app] !== null,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user