"use client"; import { useState, useEffect } from "react"; import { Card, Button, ManualConfigModal, ComboFormModal, McpMarketplaceModal, ModelSelectModal } from "@/shared/components"; import Image from "next/image"; import BaseUrlSelect from "./BaseUrlSelect"; import ApiKeySelect from "./ApiKeySelect"; const ENDPOINT = "/api/cli-tools/cowork-settings"; const stripV1 = (url) => (url || "").replace(/\/v1\/?$/, ""); const ensureV1 = (url) => { const trimmed = (url || "").replace(/\/+$/, ""); if (!trimmed) return ""; return /\/v1$/.test(trimmed) ? trimmed : `${trimmed}/v1`; }; export default function CoworkToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, hasActiveProviders, cloudEnabled, cloudUrl, tunnelEnabled, tunnelPublicUrl, tailscaleEnabled, tailscaleUrl, initialStatus, }) { 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 [selectedApiKey, setSelectedApiKey] = useState(""); const [selectedModels, setSelectedModels] = useState([]); const [showManualConfigModal, setShowManualConfigModal] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(""); const [plugins, setPlugins] = useState([]); const [localPlugins, setLocalPlugins] = useState([]); const [customPlugins, setCustomPlugins] = useState([]); const [modelAliases, setModelAliases] = useState({}); const [comboModalOpen, setComboModalOpen] = useState(false); const [modelSelectOpen, setModelSelectOpen] = useState(false); const [marketplaceOpen, setMarketplaceOpen] = useState(false); const [addMcpOpen, setAddMcpOpen] = useState(false); const [addMcpForm, setAddMcpForm] = useState({ name: "", url: "" }); useEffect(() => { if (apiKeys?.length > 0 && !selectedApiKey) { setSelectedApiKey(apiKeys[0].key); } }, [apiKeys, selectedApiKey]); useEffect(() => { if (initialStatus) setStatus(initialStatus); }, [initialStatus]); useEffect(() => { if (isExpanded && !status) checkStatus(); }, [isExpanded]); useEffect(() => { if (!isExpanded) return; fetch("/api/models/alias") .then((r) => r.ok ? r.json() : null) .then((data) => { if (data) setModelAliases(data.aliases || {}); }) .catch(() => {}); }, [isExpanded]); useEffect(() => { if (status?.cowork?.models?.length) { setSelectedModels(status.cowork.models); } if (status?.cowork?.baseUrl && !customBaseUrl) { setCustomBaseUrl(stripV1(status.cowork.baseUrl)); } // Initialize plugins: from current config, fallback to defaultPlugins if (Array.isArray(status?.cowork?.plugins) && status.cowork.plugins.length > 0) { setPlugins(status.cowork.plugins); } else if (plugins.length === 0 && Array.isArray(status?.defaultPlugins)) { setPlugins(status.defaultPlugins); } if (Array.isArray(status?.cowork?.localPlugins)) { setLocalPlugins(status.cowork.localPlugins); } if (Array.isArray(status?.cowork?.customPlugins) && status.cowork.customPlugins.length > 0) { setCustomPlugins(status.cowork.customPlugins); } }, [status]); const checkStatus = async () => { setChecking(true); try { const res = await fetch(ENDPOINT); const data = await res.json(); setStatus(data); } catch (error) { setStatus({ installed: false, error: error.message }); } finally { setChecking(false); } }; const getEffectiveBaseUrl = () => ensureV1(customBaseUrl); const getConfigStatus = () => { if (!status?.installed) return null; const url = status?.cowork?.baseUrl; if (!url) return "not_configured"; return status.has9Router ? "configured" : "other"; }; const configStatus = getConfigStatus(); const handleApply = async () => { setMessage(null); const effectiveUrl = getEffectiveBaseUrl(); if (selectedModels.length === 0) { setMessage({ type: "error", text: "Please select at least one model" }); return; } setApplying(true); try { const keyToUse = selectedApiKey?.trim() || (apiKeys?.length > 0 ? apiKeys[0].key : null) || (!cloudEnabled ? "sk_9router" : null); const res = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseUrl: effectiveUrl, apiKey: keyToUse, models: selectedModels, plugins, localPlugins, customPlugins, }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: "Settings applied. Quit & reopen Claude Desktop to load." }); checkStatus(); } else { setMessage({ type: "error", text: data.error || "Failed to apply settings" }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setApplying(false); } }; const handleCreateCombo = async ({ name, models }) => { try { const res = await fetch("/api/combos", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, models }), }); if (!res.ok) { const err = await res.json(); setMessage({ type: "error", text: err.error || "Failed to create combo" }); return; } if (!selectedModels.includes(name)) { setSelectedModels([...selectedModels, name]); } setComboModalOpen(false); setMessage({ type: "success", text: `Combo "${name}" created and added.` }); } catch (error) { setMessage({ type: "error", text: error.message }); } }; const handleAddModel = (model) => { const value = model?.value || model?.name || model; if (!value || selectedModels.includes(value)) return; setSelectedModels((prev) => [...prev, value]); }; const handleRemoveModel = (model) => { const value = model?.value || model?.name || model; setSelectedModels((prev) => prev.filter((item) => item !== value)); }; const handleReset = async () => { setRestoring(true); setMessage(null); try { const res = await fetch(ENDPOINT, { method: "DELETE" }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: "Settings reset successfully" }); setSelectedModels([]); setPlugins(status?.defaultPlugins || []); setLocalPlugins([]); setCustomPlugins([]); checkStatus(); } else { setMessage({ type: "error", text: data.error || "Failed to reset" }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setRestoring(false); } }; const addPlugin = (p) => { if (plugins.some((x) => x.name === p.name)) return; setPlugins([...plugins, p]); }; const removePlugin = (name) => { setPlugins(plugins.filter((p) => p.name !== name)); }; const getManualConfigs = () => { const keyToUse = (selectedApiKey && selectedApiKey.trim()) ? selectedApiKey : (!cloudEnabled ? "sk_9router" : ""); const modelsToShow = selectedModels.length > 0 ? selectedModels : ["provider/model-id"]; const cfg = { inferenceProvider: "gateway", inferenceGatewayBaseUrl: getEffectiveBaseUrl() || "https://your-public-host/v1", inferenceGatewayApiKey: keyToUse, inferenceModels: modelsToShow.map((name) => ({ name })), }; return [{ filename: "~/Library/Application Support/Claude-3p/configLibrary/.json", content: JSON.stringify(cfg, 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 Claude Cowork...
)} {!checking && status && !status.installed && (
warning

Claude Desktop (Cowork mode) not detected

Open Claude Desktop → Help → Troubleshooting → Enable Developer mode → Configure third-party inference, then return here.

)} {!checking && status?.installed && ( <>
Select Endpoint arrow_forward setCustomBaseUrl(stripV1(url))} tunnelEnabled={tunnelEnabled} tunnelPublicUrl={tunnelPublicUrl} tailscaleEnabled={tailscaleEnabled} tailscaleUrl={tailscaleUrl} cloudEnabled={cloudEnabled} cloudUrl={cloudUrl} />
{status?.cowork?.baseUrl && (
Current arrow_forward {status.cowork.baseUrl}
)}
API Key arrow_forward
Models arrow_forward
{selectedModels.length === 0 ? ( No models selected ) : ( selectedModels.map((m) => ( {m} )) )}
MCP arrow_forward
{/* Preset plugins */} {plugins.filter((p) => p.name !== "exa").map((p) => (
{p.title || p.name} {p.oauth && OAuth}
{Array.isArray(p.toolNames) && p.toolNames.slice(0, 6).map((t) => ( {t} ))} {Array.isArray(p.toolNames) && p.toolNames.length > 6 && ( +{p.toolNames.length - 6} )}
))} {/* Custom plugins */} {customPlugins.map((p) => (
{p.name} custom {p.url}
))} {plugins.filter((p) => p.name !== "exa").length === 0 && customPlugins.length === 0 && (
No MCPs added
)} {/* Actions row */}
Find MCPs →
Tools arrow_forward
{(() => { const exaEnabled = plugins.some((p) => p.name === "exa"); const exaDef = (status?.defaultPlugins || []).find((d) => d.name === "exa"); return ( ); })()} {(() => { const browserDef = (status?.localStdioPlugins || []).find((p) => p.name === "browsermcp"); if (!browserDef) return null; const browserEnabled = localPlugins.includes("browsermcp"); return ( ); })()}
{Array.isArray(status?.localStdioPlugins) && status.localStdioPlugins.filter((p) => p.name !== "browsermcp").length > 0 && (
Local Plugins arrow_forward
{status.localStdioPlugins.filter((p) => p.name !== "browsermcp").map((p) => { const enabled = localPlugins.includes(p.name); return ( ); })}

⚠️ Local plugins run as subprocess via npx. Requires Node.js installed.

)}
{message && (
{message.type === "success" ? "check_circle" : "error"} {message.text}
)}
)}
)} setShowManualConfigModal(false)} title="Claude Cowork - Manual Configuration" configs={getManualConfigs()} /> setComboModalOpen(false)} onSave={handleCreateCombo} activeProviders={activeProviders} forcePrefix="claude-" title="Create Cowork Combo" /> setModelSelectOpen(false)} onSelect={handleAddModel} onDeselect={handleRemoveModel} activeProviders={activeProviders} modelAliases={modelAliases} title="Select Cowork Model" addedModelValues={selectedModels} closeOnSelect={false} /> setMarketplaceOpen(false)} onAdd={addPlugin} addedNames={plugins.map((p) => p.name)} /> {/* Add Custom MCP modal */} {addMcpOpen && (
setAddMcpOpen(false)}>
e.stopPropagation()}>

Add Custom MCP

setAddMcpForm((f) => ({ ...f, name: e.target.value.replace(/\s+/g, "-").toLowerCase() }))} className="px-2 py-1.5 rounded border border-border bg-surface text-xs outline-none focus:border-primary" />
setAddMcpForm((f) => ({ ...f, url: e.target.value }))} className="px-2 py-1.5 rounded border border-border bg-surface text-xs outline-none focus:border-primary" />
)}
); }