"use client"; import { useState, useEffect, useRef } from "react"; import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; import Image from "next/image"; import BaseUrlSelect from "./BaseUrlSelect"; import ApiKeySelect from "./ApiKeySelect"; import { matchKnownEndpoint } from "./cliEndpointMatch"; export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus, tunnelEnabled, tunnelPublicUrl, tailscaleEnabled, tailscaleUrl }) { const [status, setStatus] = useState(initialStatus || null); const [checking, setChecking] = useState(false); const [applying, setApplying] = useState(false); const [restoring, setRestoring] = useState(false); const [message, setMessage] = useState(null); const [showInstallGuide, setShowInstallGuide] = useState(false); const [selectedApiKey, setSelectedApiKey] = useState(""); const [selectedModel, setSelectedModel] = useState(""); const [subagentModel, setSubagentModel] = useState(""); const [modalOpen, setModalOpen] = useState(false); const [subagentModalOpen, setSubagentModalOpen] = useState(false); const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(""); const [selectedModels, setSelectedModels] = useState([]); const [activeModel, setActiveModel] = useState(""); const selectedModelsRef = useRef([]); useEffect(() => { selectedModelsRef.current = selectedModels; }, [selectedModels]); useEffect(() => { if (apiKeys?.length > 0 && !selectedApiKey) { setSelectedApiKey(apiKeys[0].key); } }, [apiKeys, selectedApiKey]); useEffect(() => { if (initialStatus) setStatus(initialStatus); }, [initialStatus]); useEffect(() => { if (isExpanded && !status) { checkStatus(); fetchModelAliases(); } if (isExpanded) fetchModelAliases(); }, [isExpanded]); // Sync models from existing config useEffect(() => { if (status?.opencode?.models) { setSelectedModels(status.opencode.models); } if (status?.opencode?.activeModel) { setActiveModel(status.opencode.activeModel); } // Parse subagent settings from agent.explorer if exists if (status?.config?.agent?.explorer?.model?.startsWith("9router/")) { setSubagentModel(status.config.agent.explorer.model.replace("9router/", "")); } }, [status]); const fetchModelAliases = async () => { try { const res = await fetch("/api/models/alias"); const data = await res.json(); if (res.ok) setModelAliases(data.aliases || {}); } catch (error) { console.log("Error fetching model aliases:", error); } }; const saveModels = async (models) => { try { const keyToUse = (selectedApiKey && selectedApiKey.trim()) ? selectedApiKey : (!cloudEnabled ? "sk_9router" : selectedApiKey); const validActiveModel = models.includes(activeModel) ? activeModel : (models[0] || ""); await fetch("/api/cli-tools/opencode-settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, models, activeModel: validActiveModel, subagentModel, }), }); } catch (error) { console.log("Error saving models:", error); } }; const getConfigStatus = () => { if (!status?.installed) return null; if (!status.config) return "not_configured"; if (!status.has9Router) return "not_configured"; const url = status.config?.provider?.["9router"]?.options?.baseURL || ""; return matchKnownEndpoint(url, { tunnelPublicUrl, tailscaleUrl }) ? "configured" : "other"; }; const configStatus = getConfigStatus(); const getEffectiveBaseUrl = () => { const url = customBaseUrl || baseUrl; return url.endsWith("/v1") ? url : `${url}/v1`; }; const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`; const checkStatus = async () => { setChecking(true); try { const res = await fetch("/api/cli-tools/opencode-settings"); const data = await res.json(); setStatus(data); } catch (error) { setStatus({ installed: false, error: error.message }); } finally { setChecking(false); } }; const handleApply = async () => { setApplying(true); setMessage(null); try { const keyToUse = (selectedApiKey && selectedApiKey.trim()) ? selectedApiKey : (!cloudEnabled ? "sk_9router" : selectedApiKey); const res = await fetch("/api/cli-tools/opencode-settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, models: selectedModels, activeModel: activeModel === "" ? "" : (activeModel || selectedModels[0]), subagentModel: subagentModel }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: "Settings applied successfully!" }); checkStatus(); } else { setMessage({ type: "error", text: data.error || "Failed to apply settings" }); } } 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/opencode-settings", { method: "DELETE" }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: "Settings reset successfully!" }); setSelectedModel(""); setSubagentModel(""); setSelectedModels([]); setActiveModel(""); checkStatus(); } else { setMessage({ type: "error", text: data.error || "Failed to reset settings" }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setRestoring(false); } }; const getManualConfigs = () => { const keyToUse = (selectedApiKey && selectedApiKey.trim()) ? selectedApiKey : (!cloudEnabled ? "sk_9router" : ""); const modelsToShow = selectedModels.length > 0 ? selectedModels : ["provider/model-id"]; const activeModelToShow = activeModel || selectedModels[0] || modelsToShow[0]; const effectiveSubagentModel = subagentModel || activeModelToShow; const modelsObj = {}; modelsToShow.forEach(m => { modelsObj[m] = { name: m, modalities: { input: ["text", "image"], output: ["text"] } }; }); return [{ filename: "~/.config/opencode/opencode.json", content: JSON.stringify({ provider: { "9router": { npm: "@ai-sdk/openai-compatible", options: { baseURL: getEffectiveBaseUrl(), apiKey: keyToUse }, models: modelsObj, }, }, model: `9router/${activeModelToShow}`, agent: { explorer: { description: "Fast explorer subagent for codebase exploration", mode: "subagent", model: `9router/${effectiveSubagentModel}` } } }, null, 2), }]; }; return (
{tool.name} { e.target.style.display = "none"; }} />

{tool.name}

{configStatus === "configured" && Connected} {configStatus === "not_configured" && Not configured} {configStatus === "other" && Other}

{tool.description}

expand_more
{isExpanded && (
{checking && (
progress_activity Checking OpenCode CLI...
)} {!checking && status && !status.installed && (
warning

OpenCode CLI not detected locally

Manual configuration is still available if 9router is deployed on a remote server.

{showInstallGuide && (

Installation Guide

macOS / Linux:

npm install -g opencode-ai

After installation, run opencode to verify.

)}
)} {!checking && status?.installed && ( <>
{/* Current base URL */} {/* Endpoint (selector) */}
Select Endpoint arrow_forward
{/* Current configured */} {status?.config?.provider?.["9router"]?.options?.baseURL && (
Current arrow_forward {status.config.provider["9router"].options.baseURL}
)} {/* API Key */}
API Key arrow_forward
{/* Models */}
Models arrow_forward
{selectedModels.length === 0 ? ( No models selected ) : ( selectedModels.map((model) => ( { if (model === activeModel) { try { const res = await fetch("/api/cli-tools/opencode-settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clearActiveModel: true }), }); if (res.ok) { setActiveModel(""); checkStatus(); } } catch (error) { console.log("Error clearing active model:", error); } } else { setActiveModel(model); } }} className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs cursor-pointer transition-colors ${ model === activeModel ? "bg-primary/10 text-primary border border-primary" : "bg-black/5 dark:bg-white/5 text-text-muted border border-transparent hover:border-border" }`} title={model === activeModel ? "Click to clear active model" : "Click to set as active"} > {model === activeModel && star} {model} )) )}
{selectedModels.length > 0 && activeModel ? ( <>Active: {activeModel} ) : selectedModels.length > 0 ? ( Click a model to set/clear active ) : ( "Select models to add" )}
{/* Subagent Model */}
Subagent Model arrow_forward setSubagentModel(e.target.value)} placeholder={selectedModel || "provider/model-id (defaults to main model)"} className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" /> {subagentModel && ( )}
{message && (
{message.type === "success" ? "check_circle" : "error"} {message.text}
)}
)}
)} { setModalOpen(false); saveModels(selectedModelsRef.current); }} onSelect={(model) => { if (!selectedModels.includes(model.value)) { setSelectedModels([...selectedModels, model.value]); if (!activeModel) setActiveModel(model.value); } }} onDeselect={(model) => { const remaining = selectedModels.filter(m => m !== model.value); setSelectedModels(remaining); if (activeModel === model.value) { setActiveModel(remaining[0] || ""); } }} selectedModel={null} activeProviders={activeProviders} modelAliases={modelAliases} addedModelValues={selectedModels} closeOnSelect={false} title="Add Model for OpenCode" /> setSubagentModalOpen(false)} onSelect={(model) => { setSubagentModel(model.value); setSubagentModalOpen(false); }} selectedModel={subagentModel} activeProviders={activeProviders} modelAliases={modelAliases} title="Select Subagent Model for OpenCode" /> setShowManualConfigModal(false)} title="OpenCode - Manual Configuration" configs={getManualConfigs()} />
); }