"use client"; import { useState, useEffect, useRef } from "react"; import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; import ProviderIcon from "@/shared/components/ProviderIcon"; import CliStatusBadge from "./CliStatusBadge"; import { useTranslations } from "next-intl"; import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks"; const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; export default function ClineToolCard({ tool, isExpanded = false, onToggle = () => {}, baseUrl, hasActiveProviders, apiKeys, activeProviders, cloudEnabled, batchStatus, lastConfiguredAt, }) { const t = useTranslations("cliTools"); const [clineStatus, setClineStatus] = useState(null); const [checkingCline, setCheckingCline] = useState(false); const [applying, setApplying] = useState(false); const [restoring, setRestoring] = useState(false); const [message, setMessage] = useState(null); const [selectedApiKeyId, setSelectedApiKeyId] = useState(""); const [selectedModel, setSelectedModel] = useState(""); const [modalOpen, setModalOpen] = useState(false); const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(""); const hasInitializedModel = useRef(false); // Backups state const [backups, setBackups] = useState([]); const [showBackups, setShowBackups] = useState(false); const [restoringBackup, setRestoringBackup] = useState(null); const cliReady = !!(clineStatus?.installed && clineStatus?.runnable); const getConfigStatus = () => { if (!cliReady) return null; if (!clineStatus.hasOmniRoute) return "not_configured"; const baseUrlVal = clineStatus.settings?.openAiBaseUrl || ""; const localMatch = baseUrlVal.includes("localhost") || baseUrlVal.includes("127.0.0.1"); const cloudMatch = cloudEnabled && CLOUD_URL && baseUrlVal.startsWith(CLOUD_URL); if (localMatch || cloudMatch) return "configured"; return "other"; }; const configStatus = getConfigStatus(); // Use batch status as fallback when card hasn't been expanded yet const effectiveConfigStatus = configStatus || batchStatus?.configStatus || null; // (#523) Store the key *id* (not the masked string) so the backend can // resolve the real secret from DB before writing to config files. useEffect(() => { if (apiKeys?.length > 0 && !selectedApiKeyId) { setSelectedApiKeyId(apiKeys[0].id); } }, [apiKeys, selectedApiKeyId]); useEffect(() => { if (isExpanded && !clineStatus) { checkClineStatus(); fetchModelAliases(); fetchBackups(); } }, [isExpanded, clineStatus]); useEffect(() => { if (clineStatus?.settings && !hasInitializedModel.current) { const currentModel = clineStatus.settings.openAiModelId; if (currentModel) { setSelectedModel(currentModel); hasInitializedModel.current = true; } } }, [clineStatus]); const fetchModelAliases = async () => { try { const res = await fetch("/api/models/alias"); if (res.ok) { const data = await res.json(); setModelAliases(data.aliases || {}); } } catch { /* ignore */ } }; const fetchBackups = async () => { try { const res = await fetch("/api/cli-tools/backups?tool=cline"); if (res.ok) { const data = await res.json(); setBackups(data.backups || []); } } catch { /* ignore */ } }; const handleRestoreBackup = async (backupId) => { setRestoringBackup(backupId); try { const res = await fetch("/api/cli-tools/backups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tool: "cline", backupId }), }); if (res.ok) { setMessage({ type: "success", text: t("backupRestoredReloading") }); await checkClineStatus(); await fetchBackups(); } else { const data = await res.json(); setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedRestoreBackup"), }); } } catch (e) { setMessage({ type: "error", text: e.message }); } finally { setRestoringBackup(null); } }; const checkClineStatus = async () => { setCheckingCline(true); try { const res = await fetch("/api/cli-tools/cline-settings"); const data = await res.json(); setClineStatus(data); } catch (error) { setClineStatus({ error: error.message }); } finally { setCheckingCline(false); } }; const getEffectiveBaseUrl = () => { if (customBaseUrl) return customBaseUrl; return baseUrl || DEFAULT_DISPLAY_BASE_URL; }; const handleApply = async () => { setApplying(true); setMessage(null); try { const effectiveBaseUrl = getEffectiveBaseUrl(); const normalizedBaseUrl = effectiveBaseUrl.endsWith("/v1") ? effectiveBaseUrl : `${effectiveBaseUrl}/v1`; // (#523) Prefer keyId lookup so the backend writes the real key to disk. const selectedKeyId = selectedApiKeyId?.trim() || null; const res = await fetch("/api/cli-tools/cline-settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseUrl: normalizedBaseUrl, apiKey: !cloudEnabled ? "sk_omniroute" : null, keyId: selectedKeyId, model: selectedModel, }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: data.message || t("applied") }); await checkClineStatus(); await fetchBackups(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failed"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setApplying(false); } }; const handleReset = async () => { setRestoring(true); setMessage(null); try { const res = await fetch("/api/cli-tools/cline-settings", { method: "DELETE" }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: data.message || t("resetDone") }); setSelectedModel(""); hasInitializedModel.current = false; await checkClineStatus(); await fetchBackups(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failed"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setRestoring(false); } }; const handleSelectModel = (model) => { setSelectedModel(model.value); setModalOpen(false); }; const handleManualConfig = (config) => { if (config.model) setSelectedModel(config.model); // (#523) Match apiKey string to key id if possible if (config.apiKey && apiKeys?.length > 0) { const prefix = config.apiKey.slice(0, 8); const suffix = config.apiKey.slice(-4); const matchedKey = apiKeys.find( (k) => k.key && k.key.startsWith(prefix) && k.key.endsWith(suffix) ); if (matchedKey) setSelectedApiKeyId(matchedKey.id); } if (config.baseUrl) setCustomBaseUrl(config.baseUrl); setShowManualConfigModal(false); }; return (

{tool.name}

{t("toolDescriptions.cline")}

expand_more
{isExpanded && (
{checkingCline && (
progress_activity {t("checkingCli", { tool: "Cline" })}
)} {clineStatus && !checkingCline && (
{/* Runtime status */}
{cliReady ? "check_circle" : "warning"}

{cliReady ? t("cliDetectedReady", { tool: "Cline" }) : clineStatus.installed ? t("cliNotRunnable", { tool: "Cline" }) : t("cliNotDetected", { tool: "Cline" })}

{clineStatus.commandPath && (

{t("binary")}:{" "} {clineStatus.commandPath}

)} {clineStatus.globalStatePath && (

{t("configPathShort")}:{" "} {clineStatus.globalStatePath}

)}
{cliReady && ( <> {/* Current config info */} {configStatus === "configured" && (
check_circle

{t("omnirouteConfiguredOpenAiCompatible")}

{t("provider")}: openai • {t("model")}:{" "} {clineStatus.settings?.openAiModelId || "—"}

)} {/* Model selection */}
setSelectedModel(e.target.value)} placeholder={t("providerModelPlaceholder")} className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" />
{/* API Key selection */}
{apiKeys && apiKeys.length > 0 ? ( ) : (

{cloudEnabled ? t("noApiKeysAvailable") : t("usingDefaultOmniroute")}

)}
{/* Action buttons */}
{configStatus === "configured" && ( )}
{/* Message */} {message && (
{message.type === "success" ? "check_circle" : "error"} {message.text}
)} {/* Backups section */}
{showBackups && backups.length > 0 && (
{backups.map((b) => (
{b.originalFile} {new Date(b.createdAt).toLocaleString()}
))}
)} {showBackups && backups.length === 0 && (

{t("noBackupsAvailable")}

)}
)}
)}
)} setModalOpen(false)} onSelect={handleSelectModel} selectedModel={selectedModel} activeProviders={activeProviders} title={t("selectModelForTool", { tool: "Cline" })} /> {showManualConfigModal && ( setShowManualConfigModal(false)} title={t("clineManualConfiguration")} {...({ onApply: handleManualConfig, currentConfig: { model: selectedModel, apiKey: apiKeys?.find((k) => k.id === selectedApiKeyId)?.key || "", baseUrl: customBaseUrl || baseUrl, }, } as any)} /> )}
); }