diff --git a/src/app/(dashboard)/dashboard/agents/page.tsx b/src/app/(dashboard)/dashboard/agents/page.tsx new file mode 100644 index 0000000000..a61b7b7a9d --- /dev/null +++ b/src/app/(dashboard)/dashboard/agents/page.tsx @@ -0,0 +1,286 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Card, Button, Input } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +interface AgentInfo { + id: string; + name: string; + binary: string; + version: string | null; + installed: boolean; + protocol: string; + isCustom?: boolean; +} + +interface AgentSummary { + total: number; + installed: number; + notFound: number; + builtIn: number; + custom: number; +} + +export default function AgentsPage() { + const [agents, setAgents] = useState([]); + const [summary, setSummary] = useState(null); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [showAddForm, setShowAddForm] = useState(false); + const [addLoading, setAddLoading] = useState(false); + const [newAgent, setNewAgent] = useState({ + name: "", + binary: "", + versionCommand: "", + spawnArgs: "", + }); + const t = useTranslations("agents"); + + const fetchAgents = useCallback(async () => { + try { + const res = await fetch("/api/acp/agents"); + const data = await res.json(); + setAgents(data.agents || []); + setSummary(data.summary || null); + } catch (err) { + console.error("Failed to fetch agents:", err); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchAgents(); + }, [fetchAgents]); + + const handleRefresh = async () => { + setRefreshing(true); + try { + const res = await fetch("/api/acp/agents", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "refresh" }), + }); + const data = await res.json(); + setAgents(data.agents || []); + await fetchAgents(); // Re-fetch for summary + } catch (err) { + console.error("Failed to refresh:", err); + } finally { + setRefreshing(false); + } + }; + + const handleAddAgent = async (e: React.FormEvent) => { + e.preventDefault(); + setAddLoading(true); + try { + const id = newAgent.name.toLowerCase().replace(/[^a-z0-9]/g, "-"); + const res = await fetch("/api/acp/agents", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id, + name: newAgent.name, + binary: newAgent.binary, + versionCommand: newAgent.versionCommand || `${newAgent.binary} --version`, + spawnArgs: newAgent.spawnArgs ? newAgent.spawnArgs.split(",").map((s) => s.trim()) : [], + protocol: "stdio", + }), + }); + if (res.ok) { + setNewAgent({ name: "", binary: "", versionCommand: "", spawnArgs: "" }); + setShowAddForm(false); + await fetchAgents(); + } + } catch (err) { + console.error("Failed to add agent:", err); + } finally { + setAddLoading(false); + } + }; + + const handleRemoveAgent = async (agentId: string) => { + try { + const res = await fetch(`/api/acp/agents?id=${agentId}`, { method: "DELETE" }); + if (res.ok) { + await fetchAgents(); + } + } catch (err) { + console.error("Failed to remove agent:", err); + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

{t("title")}

+

{t("description")}

+
+ +
+ + {/* Summary Cards */} + {summary && ( +
+
+
{summary.installed}
+
{t("installed")}
+
+
+
{summary.notFound}
+
{t("notFound")}
+
+
+
{summary.builtIn}
+
{t("builtIn")}
+
+
+
{summary.custom}
+
{t("custom")}
+
+
+ )} + + {/* Agent Grid */} +
+ {agents.map((agent) => ( + +
+
+
+ + {agent.installed ? "smart_toy" : "block"} + +
+
+
+ {agent.name} + {agent.isCustom && ( + + {t("custom")} + + )} +
+ {agent.binary} +
+
+
+ {agent.installed ? ( + + check_circle + {agent.version || t("installed")} + + ) : ( + + cancel + {t("notFound")} + + )} +
+
+
+ + {agent.protocol} + + {agent.isCustom && ( + + )} +
+
+ ))} +
+ + {/* Add Custom Agent */} + +
+
+
+ add_circle +
+
+

{t("addCustomAgent")}

+

{t("addCustomAgentDesc")}

+
+
+ +
+ + {showAddForm && ( +
+
+ setNewAgent({ ...newAgent, name: e.target.value })} + required + /> + setNewAgent({ ...newAgent, binary: e.target.value })} + required + /> +
+
+ setNewAgent({ ...newAgent, versionCommand: e.target.value })} + /> + setNewAgent({ ...newAgent, spawnArgs: e.target.value })} + /> +
+
+ +
+
+ )} +
+
+ ); +} diff --git a/src/app/api/acp/agents/route.ts b/src/app/api/acp/agents/route.ts index 61542ed749..40ec6bdb9f 100644 --- a/src/app/api/acp/agents/route.ts +++ b/src/app/api/acp/agents/route.ts @@ -1,35 +1,121 @@ -/** - * API Route: /api/acp/agents - * - * Returns the list of detected CLI agents and their availability status. - * Used by the dashboard to show ACP transport options. - */ - import { NextResponse } from "next/server"; -import { detectInstalledAgents } from "@/lib/acp"; - -export const dynamic = "force-dynamic"; +import { + detectInstalledAgents, + refreshAgentCache, + setCustomAgents, + getCustomAgentDefs, + type CustomAgentDef, +} from "@/lib/acp/registry"; +import { getSettings, updateSettings } from "@/lib/localDb"; export async function GET() { try { + // Load custom agents from settings on each GET to stay in sync + const settings = await getSettings(); + if (settings.customAgents) { + setCustomAgents(settings.customAgents as CustomAgentDef[]); + } + const agents = detectInstalledAgents(); + const installed = agents.filter((a) => a.installed).length; + const total = agents.length; + return NextResponse.json({ - agents: agents.map((a) => ({ - id: a.id, - name: a.name, - binary: a.binary, - version: a.version, - installed: a.installed, - providerAlias: a.providerAlias, - protocol: a.protocol, - })), - available: agents.filter((a) => a.installed).length, - total: agents.length, + agents, + summary: { + total, + installed, + notFound: total - installed, + builtIn: agents.filter((a) => !a.isCustom).length, + custom: agents.filter((a) => a.isCustom).length, + }, }); - } catch (error: any) { - return NextResponse.json( - { error: error.message || "Failed to detect agents" }, - { status: 500 } - ); + } catch (error) { + console.error("Error detecting agents:", error); + return NextResponse.json({ error: "Failed to detect agents" }, { status: 500 }); + } +} + +export async function POST(request: Request) { + try { + const body = await request.json(); + + if (body.action === "refresh") { + const agents = refreshAgentCache(); + return NextResponse.json({ agents, refreshed: true }); + } + + // Add custom agent + const { id, name, binary, versionCommand, providerAlias, spawnArgs, protocol } = body; + if (!id || !name || !binary || !versionCommand) { + return NextResponse.json( + { error: "Missing required fields: id, name, binary, versionCommand" }, + { status: 400 } + ); + } + + const newAgent: CustomAgentDef = { + id: id.toLowerCase().replace(/[^a-z0-9-]/g, "-"), + name, + binary, + versionCommand, + providerAlias: providerAlias || id, + spawnArgs: spawnArgs || [], + protocol: protocol || "stdio", + }; + + // Load current, append, save + const settings = await getSettings(); + const current: CustomAgentDef[] = (settings.customAgents as CustomAgentDef[]) || []; + + // Avoid duplicates + if (current.some((a) => a.id === newAgent.id)) { + return NextResponse.json( + { error: `Agent with id '${newAgent.id}' already exists` }, + { status: 409 } + ); + } + + const updated = [...current, newAgent]; + await updateSettings({ customAgents: updated }); + setCustomAgents(updated); + + // Refresh cache to detect the new agent + const agents = refreshAgentCache(); + return NextResponse.json({ agents, added: newAgent }); + } catch (error) { + console.error("Error adding custom agent:", error); + return NextResponse.json({ error: "Failed to add agent" }, { status: 500 }); + } +} + +export async function DELETE(request: Request) { + try { + const { searchParams } = new URL(request.url); + const agentId = searchParams.get("id"); + + if (!agentId) { + return NextResponse.json({ error: "Missing agent id" }, { status: 400 }); + } + + const settings = await getSettings(); + const current: CustomAgentDef[] = (settings.customAgents as CustomAgentDef[]) || []; + const updated = current.filter((a) => a.id !== agentId); + + if (updated.length === current.length) { + return NextResponse.json( + { error: `Agent '${agentId}' not found in custom agents` }, + { status: 404 } + ); + } + + await updateSettings({ customAgents: updated }); + setCustomAgents(updated); + const agents = refreshAgentCache(); + + return NextResponse.json({ agents, removed: agentId }); + } catch (error) { + console.error("Error removing custom agent:", error); + return NextResponse.json({ error: "Failed to remove agent" }, { status: 500 }); } } diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 6747feba2f..5c50cf1810 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -101,7 +101,8 @@ "themeOrange": "أورانج (Orange)", "themeCyan": "كيان", "endpoints": "نقاط النهاية", - "playground": "ملعب النماذج" + "playground": "ملعب النماذج", + "agents": "وكلاء" }, "themesPage": { "title": "المواضيع", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "الرموز المميزة المستخدمة لإنشاء إدخالات ذاكرة التخزين المؤقت (الرجوع إلى معدل الإدخال)", "customPricingNote": "يمكنك تجاوز التسعير الافتراضي لنماذج محددة. تحظى التجاوزات المخصصة بالأولوية على الأسعار التي يتم اكتشافها تلقائيًا.", "editPricing": "تحرير التسعير", - "viewFullDetails": "عرض التفاصيل الكاملة" + "viewFullDetails": "عرض التفاصيل الكاملة", + "themeCoral": "مرجاني" }, "translator": { "title": "مترجم", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index b7044f005f..257322135f 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -101,7 +101,8 @@ "themeOrange": "Оранжево", "themeCyan": "Циан", "endpoints": "Крайни точки", - "playground": "Площадка" + "playground": "Площадка", + "agents": "Агенти" }, "themesPage": { "title": "Теми", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Токени, използвани за създаване на записи в кеша (резервен към скоростта на въвеждане)", "customPricingNote": "Можете да замените цените по подразбиране за конкретни модели. Персонализираните замени имат приоритет пред автоматично разпознатото ценообразуване.", "editPricing": "Редактиране на цените", - "viewFullDetails": "Вижте пълните подробности" + "viewFullDetails": "Вижте пълните подробности", + "themeCoral": "Корал" }, "translator": { "title": "Преводач", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 24c5da65c2..f2fbc425f9 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Turkis", "endpoints": "Endpoints", - "playground": "Legeplads" + "playground": "Legeplads", + "agents": "Agenter" }, "themesPage": { "title": "Temaer", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Tokens, der bruges til at oprette cacheposter (tilbage til inputhastighed)", "customPricingNote": "Du kan tilsidesætte standardpriser for specifikke modeller. Tilpassede tilsidesættelser har prioritet frem for automatisk registrerede priser.", "editPricing": "Rediger prissætning", - "viewFullDetails": "Se alle detaljer" + "viewFullDetails": "Se alle detaljer", + "themeCoral": "Koral" }, "translator": { "title": "Oversætter", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 2c6660e4a7..2dc4819c90 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Endpunkte", - "playground": "Spielwiese" + "playground": "Spielwiese", + "agents": "Agenten" }, "themesPage": { "title": "Themen", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Token, die zum Erstellen von Cache-Einträgen verwendet werden (Fallback auf Eingaberate)", "customPricingNote": "Sie können die Standardpreise für bestimmte Modelle überschreiben. Benutzerdefinierte Überschreibungen haben Vorrang vor automatisch erkannten Preisen.", "editPricing": "Preise bearbeiten", - "viewFullDetails": "Vollständige Details anzeigen" + "viewFullDetails": "Vollständige Details anzeigen", + "themeCoral": "Koralle" }, "translator": { "title": "Übersetzer", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 28d6175c9a..69ee31cfba 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -73,6 +73,7 @@ "settings": "Settings", "translator": "Translator", "playground": "Playground", + "agents": "Agents", "docs": "Docs", "issues": "Issues", "endpoints": "Endpoints", @@ -1748,7 +1749,8 @@ "cacheCreationTokenDesc": "Tokens used to create cache entries (fallback to input rate)", "customPricingNote": "You can override default pricing for specific models. Custom overrides take priority over auto-detected pricing.", "editPricing": "Edit Pricing", - "viewFullDetails": "View Full Details" + "viewFullDetails": "View Full Details", + "themeCoral": "Coral" }, "translator": { "title": "Translator", @@ -2421,5 +2423,22 @@ "termsSection5Text": "OmniRoute is provided \"as is\" without warranty of any kind. We are not responsible for any costs incurred through API usage, service disruptions, or data loss. Always maintain backups of your configuration.", "termsSection6Title": "6. Open Source", "termsSection6Text": "OmniRoute is open-source software. You are free to inspect, modify, and distribute it under the terms of its license." + }, + "agents": { + "title": "CLI Agents", + "description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.", + "refresh": "Refresh", + "installed": "Installed", + "notFound": "Not Found", + "builtIn": "Built-in", + "custom": "Custom", + "remove": "Remove", + "addCustomAgent": "Add Custom Agent", + "addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.", + "agentName": "Agent Name", + "binaryName": "Binary Name", + "versionCommand": "Version Command", + "spawnArgs": "Spawn Args", + "addAgent": "Add Agent" } } diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index a3041f84fb..3b69525252 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -101,7 +101,8 @@ "themeOrange": "Naranja", "themeCyan": "cian", "endpoints": "Endpoints", - "playground": "Playground" + "playground": "Playground", + "agents": "Agentes" }, "themesPage": { "title": "Temas", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Tokens utilizados para crear entradas de caché (retroceso a la tasa de entrada)", "customPricingNote": "Puede anular los precios predeterminados para modelos específicos. Las anulaciones personalizadas tienen prioridad sobre los precios detectados automáticamente.", "editPricing": "Editar precios", - "viewFullDetails": "Ver todos los detalles" + "viewFullDetails": "Ver todos los detalles", + "themeCoral": "Coral" }, "translator": { "title": "Traductor", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index feff9d6f99..706075d486 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -101,7 +101,8 @@ "themeOrange": "Oranssi", "themeCyan": "Syaani", "endpoints": "Päätepisteet", - "playground": "Leikkipaikka" + "playground": "Leikkipaikka", + "agents": "Agentit" }, "themesPage": { "title": "Teemat", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Välimuistimerkintöjen luomiseen käytetyt tunnukset (varausarvo syöttönopeuteen)", "customPricingNote": "Voit ohittaa tiettyjen mallien oletushinnoittelun. Mukautetut ohitukset ovat etusijalla automaattisesti tunnistettuihin hinnoitteluun nähden.", "editPricing": "Muokkaa hinnoittelua", - "viewFullDetails": "Näytä täydelliset tiedot" + "viewFullDetails": "Näytä täydelliset tiedot", + "themeCoral": "Koralli" }, "translator": { "title": "Kääntäjä", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 83da4be908..f8c89eebba 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Points d'accès", - "playground": "Playground" + "playground": "Playground", + "agents": "Agents" }, "themesPage": { "title": "Thèmes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Jetons utilisés pour créer des entrées de cache (repli sur le débit d'entrée)", "customPricingNote": "Vous pouvez remplacer le prix par défaut pour des modèles spécifiques. Les remplacements personnalisés ont la priorité sur les prix détectés automatiquement.", "editPricing": "Modifier le prix", - "viewFullDetails": "Afficher tous les détails" + "viewFullDetails": "Afficher tous les détails", + "themeCoral": "Corail" }, "translator": { "title": "Traducteur", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index d034a5ca65..e14e562034 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "נקודות קצה", - "playground": "Playground" + "playground": "Playground", + "agents": "סוכנים" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "אסימונים המשמשים ליצירת ערכי מטמון (חזרה לקצב קלט)", "customPricingNote": "אתה יכול לעקוף את תמחור ברירת המחדל עבור דגמים ספציפיים. עקיפות מותאמות אישית מקבלות עדיפות על פני תמחור שזוהה אוטומטית.", "editPricing": "ערוך תמחור", - "viewFullDetails": "צפה בפרטים המלאים" + "viewFullDetails": "צפה בפרטים המלאים", + "themeCoral": "אלמוג" }, "translator": { "title": "מתרגם", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index a9d628a376..bfea2cc7b1 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -101,7 +101,8 @@ "themeOrange": "narancssárga", "themeCyan": "Cián", "endpoints": "Végpontok", - "playground": "Játszótér" + "playground": "Játszótér", + "agents": "Ügynökök" }, "themesPage": { "title": "Témák", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "A gyorsítótár bejegyzéseinek létrehozására használt tokenek (vissza a beviteli sebességre)", "customPricingNote": "Egyes modelleknél felülbírálhatja az alapértelmezett árazást. Az egyéni felülbírálások elsőbbséget élveznek az automatikusan észlelt árképzéssel szemben.", "editPricing": "Árak szerkesztése", - "viewFullDetails": "Teljes részletek megtekintése" + "viewFullDetails": "Teljes részletek megtekintése", + "themeCoral": "Korall" }, "translator": { "title": "Fordító", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 72ed9769d9..45cd1e7d9a 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Endpoint", - "playground": "Playground" + "playground": "Playground", + "agents": "Agen" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Token yang digunakan untuk membuat entri cache (pengembalian ke tingkat input)", "customPricingNote": "Anda dapat mengganti harga default untuk model tertentu. Penggantian khusus lebih diprioritaskan dibandingkan harga yang terdeteksi otomatis.", "editPricing": "Sunting Harga", - "viewFullDetails": "Lihat Detail Lengkap" + "viewFullDetails": "Lihat Detail Lengkap", + "themeCoral": "Koral" }, "translator": { "title": "Penerjemah", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index 610d5d2b42..15e550d614 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Endpoint", - "playground": "Playground" + "playground": "Playground", + "agents": "एजेंट" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "कैश प्रविष्टियाँ बनाने के लिए उपयोग किए जाने वाले टोकन (इनपुट दर पर फ़ॉलबैक)", "customPricingNote": "आप विशिष्ट मॉडलों के लिए डिफ़ॉल्ट मूल्य निर्धारण को ओवरराइड कर सकते हैं। कस्टम ओवरराइड्स को स्वतः-पता लगाए गए मूल्य-निर्धारण पर प्राथमिकता दी जाती है।", "editPricing": "मूल्य निर्धारण संपादित करें", - "viewFullDetails": "पूर्ण विवरण देखें" + "viewFullDetails": "पूर्ण विवरण देखें", + "themeCoral": "कोरल" }, "translator": { "title": "अनुवादक", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index e192813955..1d622389b1 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Endpoint", - "playground": "Playground" + "playground": "Playground", + "agents": "Agenti" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Token utilizzati per creare voci nella cache (fallback alla velocità di input)", "customPricingNote": "Puoi sostituire i prezzi predefiniti per modelli specifici. Le sostituzioni personalizzate hanno la priorità sui prezzi rilevati automaticamente.", "editPricing": "Modifica prezzi", - "viewFullDetails": "Visualizza i dettagli completi" + "viewFullDetails": "Visualizza i dettagli completi", + "themeCoral": "Corallo" }, "translator": { "title": "Traduttore", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 542bb0c2ec..7b72f4c309 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "エンドポイント", - "playground": "プレイグラウンド" + "playground": "プレイグラウンド", + "agents": "エージェント" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "キャッシュ エントリの作成に使用されるトークン (入力レートへのフォールバック)", "customPricingNote": "特定のモデルのデフォルトの価格をオーバーライドできます。カスタム オーバーライドは、自動検出された価格設定よりも優先されます。", "editPricing": "価格の編集", - "viewFullDetails": "詳細を表示" + "viewFullDetails": "詳細を表示", + "themeCoral": "コーラル" }, "translator": { "title": "翻訳者", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 686f6c539e..bb4a8cdabc 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "엔드포인트", - "playground": "플레이그라운드" + "playground": "플레이그라운드", + "agents": "에이전트" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "캐시 항목을 생성하는 데 사용되는 토큰(입력 속도로 대체)", "customPricingNote": "특정 모델의 기본 가격을 재정의할 수 있습니다. 맞춤 재정의는 자동 감지된 가격보다 우선 적용됩니다.", "editPricing": "가격 편집", - "viewFullDetails": "전체 세부정보 보기" + "viewFullDetails": "전체 세부정보 보기", + "themeCoral": "코랄" }, "translator": { "title": "번역기", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 2c8a68ceb7..fc14ee0413 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Titik Akhir", - "playground": "Playground" + "playground": "Playground", + "agents": "Ejen" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Token yang digunakan untuk mencipta entri cache (sandar kepada kadar input)", "customPricingNote": "Anda boleh mengatasi harga lalai untuk model tertentu. Penggantian tersuai diutamakan berbanding harga yang dikesan secara automatik.", "editPricing": "Edit Harga", - "viewFullDetails": "Lihat Butiran Penuh" + "viewFullDetails": "Lihat Butiran Penuh", + "themeCoral": "Koral" }, "translator": { "title": "Penterjemah", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 82ec8f5601..1be295d1ee 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Eindpunten", - "playground": "Speeltuin" + "playground": "Speeltuin", + "agents": "Agenten" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Tokens die worden gebruikt om cache-items te maken (terugval op invoersnelheid)", "customPricingNote": "U kunt de standaardprijzen voor specifieke modellen overschrijven. Aangepaste overschrijvingen hebben voorrang op automatisch gedetecteerde prijzen.", "editPricing": "Prijzen bewerken", - "viewFullDetails": "Bekijk volledige details" + "viewFullDetails": "Bekijk volledige details", + "themeCoral": "Koraal" }, "translator": { "title": "Vertaler", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 5187598f0e..ac7668f5f7 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Endepunkter", - "playground": "Lekeplass" + "playground": "Lekeplass", + "agents": "Agenter" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Tokens som brukes til å lage cache-oppføringer (tilbake til inngangshastighet)", "customPricingNote": "Du kan overstyre standardpriser for spesifikke modeller. Egendefinerte overstyringer prioriteres fremfor automatisk oppdagede priser.", "editPricing": "Rediger priser", - "viewFullDetails": "Se alle detaljer" + "viewFullDetails": "Se alle detaljer", + "themeCoral": "Korall" }, "translator": { "title": "Oversetter", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index b48a8ea2d8..85403c3c30 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Mga Endpoint", - "playground": "Playground" + "playground": "Playground", + "agents": "Mga Agent" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Mga token na ginamit upang lumikha ng mga entry sa cache (fallback sa rate ng pag-input)", "customPricingNote": "Maaari mong i-override ang default na pagpepresyo para sa mga partikular na modelo. Mas inuuna ang mga custom na override kaysa sa awtomatikong natukoy na pagpepresyo.", "editPricing": "I-edit ang Pagpepresyo", - "viewFullDetails": "Tingnan ang Buong Detalye" + "viewFullDetails": "Tingnan ang Buong Detalye", + "themeCoral": "Coral" }, "translator": { "title": "Tagasalin", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index c43b8c0b47..53ae0bab48 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Punkty końcowe", - "playground": "Plac zabaw" + "playground": "Plac zabaw", + "agents": "Agenci" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Tokeny używane do tworzenia wpisów w pamięci podręcznej (powrót do szybkości wprowadzania)", "customPricingNote": "Możesz zastąpić domyślne ceny dla określonych modeli. Zastąpienia niestandardowe mają pierwszeństwo przed automatycznie wykrytymi cenami.", "editPricing": "Edytuj ceny", - "viewFullDetails": "Zobacz pełne szczegóły" + "viewFullDetails": "Zobacz pełne szczegóły", + "themeCoral": "Koral" }, "translator": { "title": "Tłumacz", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index efcffca6e0..f42ba2edac 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Endpoints", - "playground": "Playground" + "playground": "Playground", + "agents": "Agentes" }, "themesPage": { "title": "Themes", @@ -1736,7 +1737,8 @@ "cacheCreationTokenDesc": "Tokens usados para criar entradas de cache (fallback para taxa de input)", "customPricingNote": "Você pode sobrescrever preços padrão para modelos específicos. Sobrescritas personalizadas têm prioridade sobre preços detectados automaticamente.", "editPricing": "Editar Preços", - "viewFullDetails": "Ver Detalhes Completos" + "viewFullDetails": "Ver Detalhes Completos", + "themeCoral": "Coral" }, "translator": { "title": "Tradutor", @@ -2421,5 +2423,22 @@ "featureWebhooks": "Configuração de webhooks e assinaturas de eventos", "featureSwagger": "Geração automática de specs OpenAPI / Swagger", "featureAuth": "Gestão de chaves API e escopos OAuth por endpoint" + }, + "agents": { + "title": "Agentes CLI", + "description": "Descubra agentes CLI instalados no seu sistema. Adicione agentes customizados para auto-detecção.", + "refresh": "Atualizar", + "installed": "Instalado", + "notFound": "Não encontrado", + "builtIn": "Nativo", + "custom": "Customizado", + "remove": "Remover", + "addCustomAgent": "Adicionar Agente Customizado", + "addCustomAgentDesc": "Registre qualquer ferramenta CLI para detecção. Ela será verificada automaticamente ao atualizar.", + "agentName": "Nome do Agente", + "binaryName": "Nome do Binário", + "versionCommand": "Comando de Versão", + "spawnArgs": "Argumentos", + "addAgent": "Adicionar Agente" } } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index cb5045fc5b..47ddb6f78e 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -101,7 +101,8 @@ "themeViolet": "Violeta", "themeOrange": "Laranja", "themeCyan": "Ciano", - "playground": "Playground" + "playground": "Playground", + "agents": "Agentes" }, "themesPage": { "title": "Themes", @@ -1743,7 +1744,8 @@ "cacheCreationTokenDesc": "Tokens usados para criar entradas de cache (fallback para taxa de entrada)", "customPricingNote": "Você pode substituir o preço padrão de modelos específicos. As substituições personalizadas têm prioridade sobre os preços detectados automaticamente.", "editPricing": "Editar preços", - "viewFullDetails": "Ver detalhes completos" + "viewFullDetails": "Ver detalhes completos", + "themeCoral": "Coral" }, "translator": { "title": "Tradutor", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 1469badfe3..5fa092b3ab 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Puncte finale", - "playground": "Playground" + "playground": "Playground", + "agents": "Agenți" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Jetoane utilizate pentru a crea intrări în cache (retur la rata de intrare)", "customPricingNote": "Puteți suprascrie prețurile implicite pentru anumite modele. Anulările personalizate au prioritate față de prețurile detectate automat.", "editPricing": "Editați prețul", - "viewFullDetails": "Vezi detalii complete" + "viewFullDetails": "Vezi detalii complete", + "themeCoral": "Coral" }, "translator": { "title": "Traducător", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index a6fcd987c4..440020b6b5 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -101,7 +101,8 @@ "themeOrange": "Оранжевый", "themeCyan": "Голубой", "endpoints": "Конечные точки", - "playground": "Площадка" + "playground": "Площадка", + "agents": "Агенты" }, "themesPage": { "title": "Темы", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Токены, используемые для создания записей в кэше (возврат к скорости ввода)", "customPricingNote": "Вы можете переопределить цены по умолчанию для определенных моделей. Пользовательские переопределения имеют приоритет над ценами, определяемыми автоматически.", "editPricing": "Изменить цену", - "viewFullDetails": "Посмотреть полную информацию" + "viewFullDetails": "Посмотреть полную информацию", + "themeCoral": "Коралл" }, "translator": { "title": "Переводчик", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index b2a8376d60..c07ca77acc 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Koncové body", - "playground": "Ihrisko" + "playground": "Ihrisko", + "agents": "Agenti" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Tokeny používané na vytváranie záznamov vo vyrovnávacej pamäti (záložná rýchlosť vstupu)", "customPricingNote": "Predvolené ceny pre konkrétne modely môžete prepísať. Vlastné prepísania majú prednosť pred automaticky zistenými cenami.", "editPricing": "Upraviť ceny", - "viewFullDetails": "Zobraziť úplné podrobnosti" + "viewFullDetails": "Zobraziť úplné podrobnosti", + "themeCoral": "Korál" }, "translator": { "title": "Prekladateľ", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 92d9ec33ff..067f8d27f5 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Ändpunkter", - "playground": "Lekplats" + "playground": "Lekplats", + "agents": "Agenter" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Tokens som används för att skapa cacheposter (återgång till inmatningshastighet)", "customPricingNote": "Du kan åsidosätta standardpriser för specifika modeller. Anpassade åsidosättningar har prioritet framför automatiskt identifierade priser.", "editPricing": "Redigera prissättning", - "viewFullDetails": "Visa fullständiga detaljer" + "viewFullDetails": "Visa fullständiga detaljer", + "themeCoral": "Korall" }, "translator": { "title": "Översättare", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 82e42cc5b6..d1e48e72d4 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "จุดปลายทาง", - "playground": "Playground" + "playground": "Playground", + "agents": "เอเจนต์" }, "themesPage": { "title": "ธีมส์", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "โทเค็นที่ใช้ในการสร้างรายการแคช (สำรองไปยังอัตราการป้อนข้อมูล)", "customPricingNote": "คุณสามารถแทนที่ราคาเริ่มต้นสำหรับรุ่นเฉพาะได้ การแทนที่แบบกำหนดเองจะมีลำดับความสำคัญมากกว่าการกำหนดราคาที่ตรวจพบอัตโนมัติ", "editPricing": "แก้ไขราคา", - "viewFullDetails": "ดูรายละเอียดทั้งหมด" + "viewFullDetails": "ดูรายละเอียดทั้งหมด", + "themeCoral": "คอรัล" }, "translator": { "title": "นักแปล", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 301fa18ae2..dc9ed0ecb5 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Кінцеві точки", - "playground": "Playground" + "playground": "Playground", + "agents": "Агенти" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Токени, що використовуються для створення записів кешу (резервний вихід до швидкості введення)", "customPricingNote": "Ви можете змінити ціни за умовчанням для певних моделей. Спеціальні зміни мають пріоритет над автоматично визначеними цінами.", "editPricing": "Редагувати ціни", - "viewFullDetails": "Переглянути повну інформацію" + "viewFullDetails": "Переглянути повну інформацію", + "themeCoral": "Корал" }, "translator": { "title": "Перекладач", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 7b17169ec9..5dd4d43a66 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "Điểm cuối", - "playground": "Playground" + "playground": "Playground", + "agents": "Tác nhân" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "Mã thông báo được sử dụng để tạo mục nhập bộ đệm (dự phòng tốc độ đầu vào)", "customPricingNote": "Bạn có thể ghi đè giá mặc định cho các kiểu máy cụ thể. Ghi đè tùy chỉnh được ưu tiên hơn giá được tự động phát hiện.", "editPricing": "Chỉnh sửa giá", - "viewFullDetails": "Xem chi tiết đầy đủ" + "viewFullDetails": "Xem chi tiết đầy đủ", + "themeCoral": "San hô" }, "translator": { "title": "Người phiên dịch", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 7e5c3a29de..4b72b7f0f0 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -101,7 +101,8 @@ "themeOrange": "Orange", "themeCyan": "Cyan", "endpoints": "端点", - "playground": "模型试验场" + "playground": "模型试验场", + "agents": "代理" }, "themesPage": { "title": "Themes", @@ -1731,7 +1732,8 @@ "cacheCreationTokenDesc": "用于创建缓存条目的令牌(回退到输入速率)", "customPricingNote": "您可以覆盖特定型号的默认定价。自定义覆盖优先于自动检测的定价。", "editPricing": "编辑定价", - "viewFullDetails": "查看完整详情" + "viewFullDetails": "查看完整详情", + "themeCoral": "珊瑚色" }, "translator": { "title": "翻译者", diff --git a/src/lib/acp/registry.ts b/src/lib/acp/registry.ts index b9efad0d14..9721f3d00d 100644 --- a/src/lib/acp/registry.ts +++ b/src/lib/acp/registry.ts @@ -5,6 +5,8 @@ * and running version commands. Used to offer ACP transport as an alternative * to the HTTP proxy method. * + * Supports 14 built-in agents + user-defined custom agents from settings. + * * Reference: https://github.com/iOfficeAI/AionUi (auto-detects CLI agents) */ @@ -29,6 +31,19 @@ export interface CliAgentInfo { spawnArgs: string[]; /** Protocol used for communication */ protocol: "stdio" | "http"; + /** Whether this is a user-defined custom agent */ + isCustom?: boolean; +} + +/** Shape stored in settings DB for custom agents */ +export interface CustomAgentDef { + id: string; + name: string; + binary: string; + versionCommand: string; + providerAlias: string; + spawnArgs: string[]; + protocol: "stdio" | "http"; } /** @@ -80,34 +95,173 @@ const AGENT_DEFINITIONS: Omit[] = [ spawnArgs: [], protocol: "stdio", }, + { + id: "aider", + name: "Aider", + binary: "aider", + versionCommand: "aider --version", + providerAlias: "aider", + spawnArgs: ["--no-auto-commits"], + protocol: "stdio", + }, + { + id: "opencode", + name: "OpenCode", + binary: "opencode", + versionCommand: "opencode --version", + providerAlias: "opencode", + spawnArgs: [], + protocol: "stdio", + }, + { + id: "cline", + name: "Cline", + binary: "cline", + versionCommand: "cline --version", + providerAlias: "cline", + spawnArgs: [], + protocol: "stdio", + }, + { + id: "qwen-code", + name: "Qwen Code", + binary: "qwen", + versionCommand: "qwen --version", + providerAlias: "qwen", + spawnArgs: [], + protocol: "stdio", + }, + { + id: "forge", + name: "ForgeCode", + binary: "forge", + versionCommand: "forge --version", + providerAlias: "forge", + spawnArgs: [], + protocol: "stdio", + }, + { + id: "amazon-q", + name: "Amazon Q Developer", + binary: "q", + versionCommand: "q --version", + providerAlias: "amazon-q", + spawnArgs: [], + protocol: "stdio", + }, + { + id: "interpreter", + name: "Open Interpreter", + binary: "interpreter", + versionCommand: "interpreter --version", + providerAlias: "interpreter", + spawnArgs: [], + protocol: "stdio", + }, + { + id: "cursor-cli", + name: "Cursor CLI", + binary: "cursor", + versionCommand: "cursor --version", + providerAlias: "cursor", + spawnArgs: [], + protocol: "stdio", + }, + { + id: "warp", + name: "Warp AI", + binary: "warp", + versionCommand: "warp --version", + providerAlias: "warp", + spawnArgs: [], + protocol: "stdio", + }, ]; +// --------------------------------------------------------------------------- +// Detection cache (60 seconds) +// --------------------------------------------------------------------------- +let _cachedAgents: CliAgentInfo[] | null = null; +let _cacheTimestamp = 0; +const CACHE_TTL_MS = 60_000; + +/** Custom agents loaded from settings */ +let _customAgentDefs: CustomAgentDef[] = []; + +/** + * Set custom agent definitions from settings. + */ +export function setCustomAgents(agents: CustomAgentDef[]): void { + _customAgentDefs = agents || []; + _cachedAgents = null; // invalidate cache +} + +/** + * Get current custom agent definitions. + */ +export function getCustomAgentDefs(): CustomAgentDef[] { + return _customAgentDefs; +} + +/** + * Detect a single agent by running its version command. + */ +function detectAgent( + def: Omit, + isCustom = false +): CliAgentInfo { + let version: string | null = null; + let installed = false; + + try { + const output = execSync(def.versionCommand, { + timeout: 5000, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + + // Extract version number from output + const versionMatch = output.match(/(\d+\.\d+\.\d+(?:-\w+)?)/); + version = versionMatch ? versionMatch[1] : output.split("\n")[0]; + installed = true; + } catch { + // Not installed or not runnable + } + + return { ...def, version, installed, isCustom }; +} + /** * Detect installed CLI agents on the system. - * Runs version commands to verify availability. + * Results are cached for 60 seconds. */ export function detectInstalledAgents(): CliAgentInfo[] { - return AGENT_DEFINITIONS.map((def) => { - let version: string | null = null; - let installed = false; + const now = Date.now(); + if (_cachedAgents && now - _cacheTimestamp < CACHE_TTL_MS) { + return _cachedAgents; + } - try { - const output = execSync(def.versionCommand, { - timeout: 5000, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }).trim(); + // Merge built-in + custom definitions + const allDefs = [ + ...AGENT_DEFINITIONS.map((d) => ({ ...d, _custom: false })), + ..._customAgentDefs.map((d) => ({ ...d, _custom: true })), + ]; - // Extract version number from output - const versionMatch = output.match(/(\d+\.\d+\.\d+(?:-\w+)?)/); - version = versionMatch ? versionMatch[1] : output.split("\n")[0]; - installed = true; - } catch { - // Not installed or not runnable - } - - return { ...def, version, installed }; + _cachedAgents = allDefs.map((def) => { + const { _custom, ...rest } = def; + return detectAgent(rest, _custom); }); + _cacheTimestamp = now; + + return _cachedAgents; +} + +/** + * Force refresh detection cache. + */ +export function refreshAgentCache(): CliAgentInfo[] { + _cachedAgents = null; + return detectInstalledAgents(); } /** diff --git a/src/shared/components/Sidebar.tsx b/src/shared/components/Sidebar.tsx index 8e6cf5582d..0a177e32ce 100644 --- a/src/shared/components/Sidebar.tsx +++ b/src/shared/components/Sidebar.tsx @@ -30,6 +30,7 @@ const navItemDefs = [ const debugItemDefs = [ { href: "/dashboard/translator", i18nKey: "translator", icon: "translate" }, { href: "/dashboard/playground", i18nKey: "playground", icon: "science" }, + { href: "/dashboard/agents", i18nKey: "agents", icon: "smart_toy" }, ]; const systemItemDefs = [{ href: "/dashboard/settings", i18nKey: "settings", icon: "settings" }]; diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index db22d1bdde..0029677c90 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -36,4 +36,18 @@ export const updateSettingsSchema = z.object({ a2aEnabled: z.boolean().optional(), // CLI Fingerprint compatibility (per-provider) cliCompatProviders: z.array(z.string().max(100)).optional(), + // Custom CLI agent definitions for ACP + customAgents: z + .array( + z.object({ + id: z.string().max(50), + name: z.string().max(100), + binary: z.string().max(200), + versionCommand: z.string().max(300), + providerAlias: z.string().max(50), + spawnArgs: z.array(z.string().max(200)), + protocol: z.enum(["stdio", "http"]), + }) + ) + .optional(), });