"use client"; import { useState, useEffect, useRef } from "react"; import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; import Image from "next/image"; 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 KiloToolCard({ tool, isExpanded = false, onToggle = () => {}, baseUrl, hasActiveProviders, apiKeys, activeProviders, cloudEnabled, batchStatus, lastConfiguredAt, }) { const t = useTranslations("cliTools"); const [kiloStatus, setKiloStatus] = useState(null); const [checkingKilo, setCheckingKilo] = 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 = !!(kiloStatus?.installed && kiloStatus?.runnable); const getConfigStatus = () => { if (!cliReady) return null; if (!kiloStatus.hasOmniRoute) return "not_configured"; return "configured"; }; 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 && !kiloStatus) { checkKiloStatus(); fetchModelAliases(); fetchBackups(); } }, [isExpanded, kiloStatus]); 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=kilo"); 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: "kilo", backupId }), }); if (res.ok) { setMessage({ type: "success", text: t("backupRestoredReloading") }); await checkKiloStatus(); 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 checkKiloStatus = async () => { setCheckingKilo(true); try { const res = await fetch("/api/cli-tools/kilo-settings"); const data = await res.json(); setKiloStatus(data); } catch (error) { setKiloStatus({ error: error.message }); } finally { setCheckingKilo(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/kilo-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 checkKiloStatus(); 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/kilo-settings", { method: "DELETE" }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: data.message || t("resetDone") }); setSelectedModel(""); hasInitializedModel.current = false; await checkKiloStatus(); 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.image ? ( {tool.name} { (e.currentTarget as HTMLElement).style.display = "none"; }} /> ) : ( terminal )}

{tool.name}

{t("toolDescriptions.kilo")}

expand_more
{isExpanded && (
{checkingKilo && (
progress_activity {t("checkingCli", { tool: "Kilo Code" })}
)} {kiloStatus && !checkingKilo && (
{/* Runtime status */}
{cliReady ? "check_circle" : "warning"}

{cliReady ? t("cliDetectedReady", { tool: "Kilo Code" }) : kiloStatus.installed ? t("cliNotRunnable", { tool: "Kilo Code" }) : t("cliNotDetected", { tool: "Kilo Code" })}

{kiloStatus.commandPath && (

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

)} {kiloStatus.authPath && (

{t("auth")}:{" "} {kiloStatus.authPath}

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

{t("omnirouteConfiguredOpenAiCompatible")}

{t("providers")}:{" "} {kiloStatus.settings?.auth?.join(", ") || "—"}

)} {/* 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: "Kilo Code" })} /> {showManualConfigModal && ( setShowManualConfigModal(false)} title={t("kiloManualConfiguration")} {...({ onApply: handleManualConfig, currentConfig: { model: selectedModel, apiKey: apiKeys?.find((k) => k.id === selectedApiKeyId)?.key || "", baseUrl: customBaseUrl || baseUrl, }, } as any)} /> )}
); }