Files
farion1231--cc-switch/src/components/AppSwitcher.tsx
T
wehub-resource-sync 64ed910d3f
CI / Backend Checks (push) Has been cancelled
CI / Frontend Checks (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:57:20 +08:00

131 lines
3.8 KiB
TypeScript

import type { AppId } from "@/lib/api";
import type { VisibleApps } from "@/types";
import { ProviderIcon } from "@/components/ProviderIcon";
import { cn } from "@/lib/utils";
import { Monitor, Terminal } from "lucide-react";
const APP_BADGE_ICON: Partial<
Record<AppId, { icon: typeof Terminal; offsetY?: number }>
> = {
claude: { icon: Terminal },
"claude-desktop": { icon: Monitor, offsetY: 0.5 },
};
interface AppSwitcherProps {
activeApp: AppId;
onSwitch: (app: AppId) => void;
visibleApps?: VisibleApps;
compact?: boolean;
}
const ALL_APPS: AppId[] = [
"claude",
"claude-desktop",
"codex",
"gemini",
"opencode",
"openclaw",
"hermes",
];
const STORAGE_KEY = "cc-switch-last-app";
export function AppSwitcher({
activeApp,
onSwitch,
visibleApps,
compact,
}: AppSwitcherProps) {
const handleSwitch = (app: AppId) => {
if (app === activeApp) return;
localStorage.setItem(STORAGE_KEY, app);
onSwitch(app);
};
const iconSize = 20;
const appIconName: Record<AppId, string> = {
claude: "claude",
"claude-desktop": "claude",
codex: "openai",
gemini: "gemini",
opencode: "opencode",
openclaw: "openclaw",
hermes: "hermes",
};
const appDisplayName: Record<AppId, string> = {
claude: "Claude Code",
"claude-desktop": "Claude Desktop",
codex: "Codex",
gemini: "Gemini",
opencode: "OpenCode",
openclaw: "OpenClaw",
hermes: "Hermes",
};
// Filter apps based on visibility settings (default all visible)
const appsToShow = ALL_APPS.filter((app) => {
if (!visibleApps) return true;
return visibleApps[app];
});
return (
<div className="inline-flex bg-muted rounded-xl p-1 gap-1">
{appsToShow.map((app) => {
const badgeConfig = APP_BADGE_ICON[app];
const BadgeIcon = badgeConfig?.icon;
const isActive = activeApp === app;
return (
<button
key={app}
type="button"
onClick={() => handleSwitch(app)}
className={cn(
"group inline-flex items-center px-3 h-8 rounded-md text-sm font-medium transition-all duration-200",
isActive
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50",
)}
>
<span className="relative inline-flex shrink-0">
<ProviderIcon
icon={appIconName[app]}
name={appDisplayName[app]}
size={iconSize}
/>
{BadgeIcon && (
<span
className={cn(
"absolute -bottom-0.5 -right-0.5 flex items-center justify-center rounded-[3px] border h-[11px] w-[11px]",
isActive
? "bg-background border-border text-foreground"
: "bg-muted border-background text-muted-foreground group-hover:bg-background group-hover:text-foreground",
)}
aria-hidden="true"
>
<BadgeIcon
className="h-[8px] w-[8px]"
strokeWidth={2.5}
style={
badgeConfig?.offsetY
? { transform: `translateY(${badgeConfig.offsetY}px)` }
: undefined
}
/>
</span>
)}
</span>
<span
className={cn(
"transition-all duration-200 whitespace-nowrap overflow-hidden",
compact
? "max-w-0 opacity-0 ml-0"
: "max-w-[120px] opacity-100 ml-2",
)}
>
{appDisplayName[app]}
</span>
</button>
);
})}
</div>
);
}