Files
cc-switch/src/components/providers/ProviderActions.tsx
T
w0x7ce 3dcbe313be feat: add provider-specific terminal button
Add a terminal button next to each provider card that opens a new terminal
window with that provider's specific API configuration. This allows using
different providers independently without changing the global setting.

Changes:
- Backend: Add `open_provider_terminal` command that extracts provider
  config and creates a temporary claude settings file
- Frontend: Add terminal button to provider cards with proper callback
  propagation through component hierarchy
- Support macOS (Terminal.app), Linux (gnome-terminal, konsole, etc.),
  and Windows (cmd)

Each provider gets a unique config file named `claude_<providerId>_<pid>.json`
in the temp directory, containing the provider's API configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-23 17:11:20 +08:00

209 lines
5.4 KiB
TypeScript

import {
BarChart3,
Check,
Copy,
Edit,
Loader2,
Play,
Plus,
Terminal,
TestTube2,
Trash2,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ProviderActionsProps {
isCurrent: boolean;
isTesting?: boolean;
isProxyTakeover?: boolean;
onSwitch: () => void;
onEdit: () => void;
onDuplicate: () => void;
onTest?: () => void;
onConfigureUsage: () => void;
onDelete: () => void;
onOpenTerminal?: () => void;
// 故障转移相关
isAutoFailoverEnabled?: boolean;
isInFailoverQueue?: boolean;
onToggleFailover?: (enabled: boolean) => void;
}
export function ProviderActions({
isCurrent,
isTesting,
isProxyTakeover = false,
onSwitch,
onEdit,
onDuplicate,
onTest,
onConfigureUsage,
onDelete,
onOpenTerminal,
// 故障转移相关
isAutoFailoverEnabled = false,
isInFailoverQueue = false,
onToggleFailover,
}: ProviderActionsProps) {
const { t } = useTranslation();
const iconButtonClass = "h-8 w-8 p-1";
// 故障转移模式下的按钮逻辑
const isFailoverMode = isAutoFailoverEnabled && onToggleFailover;
// 处理主按钮点击
const handleMainButtonClick = () => {
if (isFailoverMode) {
// 故障转移模式:切换队列状态
onToggleFailover(!isInFailoverQueue);
} else {
// 普通模式:切换供应商
onSwitch();
}
};
// 主按钮的状态和样式
const getMainButtonState = () => {
if (isFailoverMode) {
// 故障转移模式
if (isInFailoverQueue) {
return {
disabled: false,
variant: "secondary" as const,
className:
"bg-blue-100 text-blue-600 hover:bg-blue-200 dark:bg-blue-900/50 dark:text-blue-400 dark:hover:bg-blue-900/70",
icon: <Check className="h-4 w-4" />,
text: t("failover.inQueue", { defaultValue: "已加入" }),
};
}
return {
disabled: false,
variant: "default" as const,
className:
"bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700",
icon: <Plus className="h-4 w-4" />,
text: t("failover.addQueue", { defaultValue: "加入" }),
};
}
// 普通模式
if (isCurrent) {
return {
disabled: true,
variant: "secondary" as const,
className:
"bg-gray-200 text-muted-foreground hover:bg-gray-200 hover:text-muted-foreground dark:bg-gray-700 dark:hover:bg-gray-700",
icon: <Check className="h-4 w-4" />,
text: t("provider.inUse"),
};
}
return {
disabled: false,
variant: "default" as const,
className: isProxyTakeover
? "bg-emerald-500 hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700"
: "",
icon: <Play className="h-4 w-4" />,
text: t("provider.enable"),
};
};
const buttonState = getMainButtonState();
return (
<div className="flex items-center gap-1.5">
<Button
size="sm"
variant={buttonState.variant}
onClick={handleMainButtonClick}
disabled={buttonState.disabled}
className={cn("w-[4.5rem] px-2.5", buttonState.className)}
>
{buttonState.icon}
{buttonState.text}
</Button>
<div className="flex items-center gap-1">
<Button
size="icon"
variant="ghost"
onClick={onEdit}
title={t("common.edit")}
className={iconButtonClass}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
onClick={onDuplicate}
title={t("provider.duplicate")}
className={iconButtonClass}
>
<Copy className="h-4 w-4" />
</Button>
{onTest && (
<Button
size="icon"
variant="ghost"
onClick={onTest}
disabled={isTesting}
title={t("modelTest.testProvider", "测试模型")}
className={iconButtonClass}
>
{isTesting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<TestTube2 className="h-4 w-4" />
)}
</Button>
)}
<Button
size="icon"
variant="ghost"
onClick={onConfigureUsage}
title={t("provider.configureUsage")}
className={iconButtonClass}
>
<BarChart3 className="h-4 w-4" />
</Button>
{onOpenTerminal && (
<Button
size="icon"
variant="ghost"
onClick={onOpenTerminal}
title={t("provider.openTerminal", "打开终端")}
className={cn(
iconButtonClass,
"hover:text-emerald-600 dark:hover:text-emerald-400",
)}
>
<Terminal className="h-4 w-4" />
</Button>
)}
<Button
size="icon"
variant="ghost"
onClick={isCurrent ? undefined : onDelete}
title={t("common.delete")}
className={cn(
iconButtonClass,
!isCurrent && "hover:text-red-500 dark:hover:text-red-400",
isCurrent && "opacity-40 cursor-not-allowed text-muted-foreground",
)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
);
}