"use client"; import { useState, useEffect } from "react"; import { Card, Button, Badge, Modal, Input, ModelSelectModal } from "@/shared/components"; import { useTranslations } from "next-intl"; import ProviderIcon from "@/shared/components/ProviderIcon"; export default function AntigravityToolCard({ tool, isExpanded = false, onToggle = () => {}, baseUrl, apiKeys, activeProviders, hasActiveProviders, cloudEnabled, }) { const t = useTranslations("cliTools"); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false); const [sudoPassword, setSudoPassword] = useState(""); const [selectedApiKeyId, setSelectedApiKeyId] = useState(""); const [message, setMessage] = useState(null); const [modelMappings, setModelMappings] = useState({}); const [modalOpen, setModalOpen] = useState(false); const [currentEditingAlias, setCurrentEditingAlias] = useState(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 && !status) { fetchStatus(); loadSavedMappings(); } }, [isExpanded, status]); const loadSavedMappings = async () => { try { const res = await fetch(`/api/cli-tools/antigravity-mitm/alias?tool=${tool.id}`); if (res.ok) { const data = await res.json(); const aliases = data.aliases || {}; if (Object.keys(aliases).length > 0) { setModelMappings(aliases); } } } catch (error) { console.log("Error loading saved mappings:", error); } }; const fetchStatus = async () => { try { const res = await fetch("/api/cli-tools/antigravity-mitm"); if (res.ok) { const data = await res.json(); setStatus(data); } } catch (error) { console.log("Error fetching status:", error); setStatus({ running: false }); } }; // Windows uses UAC dialog, no sudo needed const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows"); const handleStart = () => { if (isWindows || status?.hasCachedPassword) { doStart(""); } else { setShowPasswordModal(true); setMessage(null); } }; const handleStop = () => { if (isWindows || status?.hasCachedPassword) { doStop(""); } else { setShowPasswordModal(true); setMessage(null); } }; const doStart = async (password) => { setLoading(true); setMessage(null); try { // (#523) Prefer keyId lookup so the backend writes the real key to disk. const selectedKeyId = selectedApiKeyId?.trim() || (apiKeys?.length > 0 ? apiKeys[0].id : null); const res = await fetch("/api/cli-tools/antigravity-mitm", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apiKey: !cloudEnabled ? "sk_omniroute" : null, keyId: selectedKeyId, sudoPassword: password, }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("mitmStarted") }); setShowPasswordModal(false); setSudoPassword(""); fetchStatus(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedStart"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setLoading(false); } }; const doStop = async (password) => { setLoading(true); setMessage(null); try { const res = await fetch("/api/cli-tools/antigravity-mitm", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sudoPassword: password }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("mitmStopped") }); setShowPasswordModal(false); setSudoPassword(""); fetchStatus(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedStop"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setLoading(false); } }; const handleConfirmPassword = () => { if (!sudoPassword.trim()) { setMessage({ type: "error", text: t("sudoPasswordRequiredError") }); return; } if (status?.running) { doStop(sudoPassword); } else { doStart(sudoPassword); } }; const openModelSelector = (alias) => { setCurrentEditingAlias(alias); setModalOpen(true); }; const handleModelSelect = (model) => { if (currentEditingAlias) { setModelMappings((prev) => ({ ...prev, [currentEditingAlias]: model.value, })); } }; const handleModelMappingChange = (alias, value) => { setModelMappings((prev) => ({ ...prev, [alias]: value, })); }; const handleSaveMappings = async () => { setLoading(true); setMessage(null); try { const res = await fetch("/api/cli-tools/antigravity-mitm/alias", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tool: tool.id, mappings: modelMappings }), }); if (!res.ok) { const data = await res.json(); throw new Error( (typeof data.error === "string" ? data.error : data.error?.message) || t("failedSaveMappings") ); } setMessage({ type: "success", text: t("mappingsSaved") }); } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setLoading(false); } }; const isRunning = status?.running; return (

{tool.name}

{isRunning ? ( {t("active")} ) : ( {t("inactive")} )}

{tool.description}

expand_more
{isExpanded && (
{/* Start/Stop Button - always on top */}
{isRunning ? ( ) : ( )}
{message?.type === "error" && (
error {message.text}
)} {/* When running: API Key + Model Mappings */} {isRunning && ( <>
{t("apiKey")} arrow_forward {apiKeys.length > 0 ? ( ) : ( {cloudEnabled ? t("noApiKeysCreateOne") : t("defaultOmnirouteKey")} )}
{(tool.defaultModels || []).map((model) => (
{model.name} arrow_forward handleModelMappingChange(model.alias, e.target.value)} placeholder={t("modelPlaceholder")} className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> {modelMappings[model.alias] && ( )}
))}
)} {/* When stopped: how it works */} {!isRunning && (() => { // Dynamic MITM instructions per tool (#505) const mitmDomains: Record = { antigravity: "daily-cloudcode-pa.googleapis.com", kiro: "api.anthropic.com", }; const toolName = tool.name || tool.id; const domain = mitmDomains[tool.id] || mitmDomains.antigravity; return (

{t("howItWorks")}{" "} {t("mitmHowWorksDesc", { toolName })}

{t("mitmStep1")} {t("mitmStep2Prefix")}{" "} {domain}{" "} {t("mitmStep2Suffix")} {t("mitmStep3", { toolName })}
); })()}
)} {/* Password Modal */} { setShowPasswordModal(false); setSudoPassword(""); setMessage(null); }} title={t("sudoPasswordRequiredTitle")} size="sm" >
warning

{t("sudoPasswordHint")}

setSudoPassword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !loading) handleConfirmPassword(); }} /> {message && (
{message.type === "success" ? "check_circle" : "error"} {message.text}
)}
{/* Model Select Modal */} setModalOpen(false)} onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} title={t("selectModelForAlias", { alias: currentEditingAlias || "" })} />
); }