mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Adds a new ClaudeDesktop AppType that writes Claude Desktop's third-party
inference profile under configLibrary/, sharing _meta.json with other
launchers (Ollama-compatible) so cc-switch can coexist with them.
Two switch modes:
- direct: provider already exposes claude-* / anthropic/claude-* model
ids on Anthropic Messages, Claude Desktop connects to it directly.
- proxy: cc-switch's local proxy acts as the inference gateway,
presenting only claude-* route names to Claude Desktop and mapping
them to real upstream models. Required after Anthropic restricted
Claude Desktop to claude-family ids.
Backend:
- New module claude_desktop_config with snapshot/rollback, official seed
bypass, /claude-desktop/v1/{models,messages} routes, and a single
source of truth for default proxy routes.
- Gateway token persisted in SQLite, validated on every proxied request.
- get_claude_desktop_status surfaces drift signals (stale models,
missing routes, proxy stopped, base URL mismatch, missing token).
Frontend:
- Slim ClaudeDesktopProviderForm independent from ProviderForm,
controlled by a top-level appId guard.
- ProviderList banner consumes the status query (5s polling) and
renders actionable diagnostics.
- ClaudeDesktopRouteToggle in the header to start/stop the local
gateway without touching takeover state.
- Three-locale i18n synchronised.
134 lines
3.4 KiB
TypeScript
134 lines
3.4 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { ProviderIcon } from "@/components/ProviderIcon";
|
|
import type { SettingsFormState } from "@/hooks/useSettings";
|
|
import type { VisibleApps } from "@/types";
|
|
import type { AppId } from "@/lib/api";
|
|
|
|
interface AppVisibilitySettingsProps {
|
|
settings: SettingsFormState;
|
|
onChange: (updates: Partial<SettingsFormState>) => void;
|
|
}
|
|
|
|
const APP_CONFIG: Array<{
|
|
id: AppId;
|
|
icon: string;
|
|
nameKey: string;
|
|
}> = [
|
|
{ id: "claude", icon: "claude", nameKey: "apps.claude" },
|
|
{
|
|
id: "claude-desktop",
|
|
icon: "claude",
|
|
nameKey: "apps.claudeDesktop",
|
|
},
|
|
{ id: "codex", icon: "openai", nameKey: "apps.codex" },
|
|
{ id: "gemini", icon: "gemini", nameKey: "apps.gemini" },
|
|
{ id: "opencode", icon: "opencode", nameKey: "apps.opencode" },
|
|
{ id: "openclaw", icon: "openclaw", nameKey: "apps.openclaw" },
|
|
{ id: "hermes", icon: "hermes", nameKey: "apps.hermes" },
|
|
];
|
|
|
|
export function AppVisibilitySettings({
|
|
settings,
|
|
onChange,
|
|
}: AppVisibilitySettingsProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const visibleApps: VisibleApps = settings.visibleApps ?? {
|
|
claude: true,
|
|
"claude-desktop": true,
|
|
codex: true,
|
|
gemini: true,
|
|
opencode: true,
|
|
openclaw: true,
|
|
hermes: true,
|
|
};
|
|
|
|
// Count how many apps are currently visible
|
|
const visibleCount = Object.values(visibleApps).filter(Boolean).length;
|
|
|
|
const handleToggle = (appId: AppId) => {
|
|
const isCurrentlyVisible = visibleApps[appId];
|
|
// Prevent disabling the last visible app
|
|
if (isCurrentlyVisible && visibleCount <= 1) return;
|
|
|
|
onChange({
|
|
visibleApps: {
|
|
...visibleApps,
|
|
[appId]: !isCurrentlyVisible,
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<section className="space-y-2">
|
|
<header className="space-y-1">
|
|
<h3 className="text-sm font-medium">
|
|
{t("settings.appVisibility.title")}
|
|
</h3>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t("settings.appVisibility.description")}
|
|
</p>
|
|
</header>
|
|
<div className="inline-flex gap-1 rounded-md border border-border-default bg-background p-1">
|
|
{APP_CONFIG.map((app) => {
|
|
const isVisible = visibleApps[app.id];
|
|
// Disable button if this is the last visible app
|
|
const isDisabled = isVisible && visibleCount <= 1;
|
|
|
|
return (
|
|
<AppButton
|
|
key={app.id}
|
|
active={isVisible}
|
|
disabled={isDisabled}
|
|
onClick={() => handleToggle(app.id)}
|
|
icon={app.icon}
|
|
name={t(app.nameKey)}
|
|
>
|
|
{t(app.nameKey)}
|
|
</AppButton>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
interface AppButtonProps {
|
|
active: boolean;
|
|
disabled?: boolean;
|
|
onClick: () => void;
|
|
icon: string;
|
|
name: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
function AppButton({
|
|
active,
|
|
disabled,
|
|
onClick,
|
|
icon,
|
|
name,
|
|
children,
|
|
}: AppButtonProps) {
|
|
return (
|
|
<Button
|
|
type="button"
|
|
onClick={onClick}
|
|
disabled={disabled}
|
|
size="sm"
|
|
variant={active ? "default" : "ghost"}
|
|
className={cn(
|
|
"min-w-[90px] w-auto gap-1.5 px-3",
|
|
active
|
|
? "shadow-sm"
|
|
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
)}
|
|
>
|
|
<ProviderIcon icon={icon} name={name} size={14} />
|
|
{children}
|
|
</Button>
|
|
);
|
|
}
|