// ClawdDotNet — Agenten-Verwaltung // // Übersicht, Anlegen, Löschen von Agenten, Werkzeug-Schalter und maskierte Einstellungen. import { api } from "../modules/api.js"; import { abonnieren } from "../modules/events.js"; import { t } from "../modules/i18n.js"; import { erstelleElement, erstelleIcon, PFADE, erstellePlakette, erstelleKarte, erstelleModal, zeigeMeldung } from "../modules/dom.js"; let gewaehlterAgentId = null; let agenten = []; let entladeEreignisse = []; export async function rendereAgenten(parameter = {}) { const container = document.getElementById("ansicht-inhalt"); if (!container) return; container.replaceChildren(); entladeEreignisse.forEach((c) => c()); entladeEreignisse = []; const layout = erstelleElement("div", { klasse: "agenten-ansicht" }); const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" }); kopfZeile.append( erstelleElement("h1", { text: t("agents.title") }), erstelleElement( "button", { klasse: "schaltflaeche primaer", onclick: oeffneNeuerAgentDialog }, erstelleIcon(PFADE.plus, 16), erstelleElement("span", { text: ` ${t("agents.newAgent")}` }) ) ); const raster = erstelleElement("div", { klasse: "agenten-raster" }); const listeContainer = erstelleElement("div", { klasse: "agenten-liste-bereich" }); const detailContainer = erstelleElement("div", { klasse: "agenten-detail-bereich" }); raster.append(listeContainer, detailContainer); layout.append(kopfZeile, raster); container.append(layout); await ladeUndRendereListe(parameter.agentId); entladeEreignisse.push( abonnieren("instance", (daten) => { if (daten.kind === "Agents") { ladeUndRendereListe(gewaehlterAgentId); } }) ); } async function ladeUndRendereListe(zielId = null) { const listeContainer = document.querySelector(".agenten-liste-bereich"); if (!listeContainer) return; listeContainer.replaceChildren( erstelleElement("div", { klasse: "hinweis-zentriert", text: t("agents.loading") }) ); try { agenten = await api("/agents"); listeContainer.replaceChildren(); if (!agenten || agenten.length === 0) { const leerKasten = erstelleElement("div", { klasse: "hinweis-zentriert" }); leerKasten.append( erstelleElement("p", { klasse: "gedaempft", text: t("agents.empty") }), erstelleElement("button", { klasse: "schaltflaeche primaer klein", onclick: oeffneNeuerAgentDialog, style: "margin-top: 10px;" }, erstelleIcon(PFADE.plus, 14), erstelleElement("span", { text: ` ${t("agents.createFirst")}` })) ); listeContainer.append(leerKasten); rendereDetails(null); return; } const auswahlId = zielId && agenten.some((a) => a.agentId === zielId) ? zielId : agenten[0].agentId; for (const a of agenten) { const eintrag = erstelleElement("div", { klasse: `agenten-karte ${a.agentId === auswahlId ? "aktiv" : ""}`, onclick: () => { document.querySelectorAll(".agenten-karte").forEach((k) => k.classList.remove("aktiv")); eintrag.classList.add("aktiv"); rendereDetails(a.agentId); } }); const top = erstelleElement("div", { klasse: "agent-karte-top" }); const name = erstelleElement("strong", { text: a.displayName }); const status = a.isRunning ? erstellePlakette(t("status.busy"), "warnung") : erstellePlakette(t("status.ready"), "neutral"); top.append(name, status); const meta = erstelleElement("div", { klasse: "agent-karte-meta" }); meta.append( erstelleElement("span", { klasse: "gedaempft", text: `${t("agents.id")}: ${a.agentId}` }), erstelleElement("span", { klasse: "gedaempft", text: ` · ${t("agents.model")}: ${a.model}` }) ); const werkzeugeZaehler = a.tools ? a.tools.filter((t) => t.enabled).length : 0; const werkzeugeText = erstelleElement("div", { klasse: "agent-karte-werkzeuge gedaempft", text: t("agents.toolsActive", { count: werkzeugeZaehler }) }); eintrag.append(top, meta, werkzeugeText); listeContainer.append(eintrag); } rendereDetails(auswahlId); } catch (err) { const text = err.instanzUnerreichbar ? (t("instances.instanceOfflineError") || err.message) : (err.instanzKonflikt ? (t("instances.noInstanceActiveError") || err.message) : err.message); listeContainer.replaceChildren( erstelleElement("div", { klasse: "fehler-zentriert", text }) ); } } async function rendereDetails(agentId) { gewaehlterAgentId = agentId; const detailContainer = document.querySelector(".agenten-detail-bereich"); if (!detailContainer) return; detailContainer.replaceChildren(); if (!agentId) { detailContainer.append( erstelleKarte(t("agents.details"), [ erstelleElement("p", { klasse: "gedaempft", text: t("agents.selectPrompt") }) ]) ); return; } const agent = agenten.find((a) => a.agentId === agentId); if (!agent) return; // Kopf-Aktionen (Chatten, Löschen) const aktionen = erstelleElement("div", { klasse: "gruppe-horizontal" }); const zumChatBtn = erstelleElement( "button", { klasse: "schaltflaeche flach", title: t("agents.chatTooltip"), onclick: () => { window.location.hash = `#/chat/${encodeURIComponent(agent.agentId)}`; } }, erstelleIcon(PFADE.chat, 14), erstelleElement("span", { text: ` ${t("agents.chat")}` }) ); const loeschenBtn = erstelleElement( "button", { klasse: "schaltflaeche gefahr flach", title: t("agents.deleteTooltip"), onclick: () => loescheAgent(agent.agentId, agent.displayName) }, erstelleIcon(PFADE.muelleimer, 14), erstelleElement("span", { text: ` ${t("agents.delete")}` }) ); aktionen.append(zumChatBtn, loeschenBtn); // Stammdaten-Karte const stammdatenListe = erstelleElement("dl", { klasse: "datenliste" }); const felder = [ [t("agents.name"), agent.displayName], [t("agents.id"), agent.agentId], [t("agents.model"), agent.model], [t("agents.description"), agent.description || "—"] ]; for (const [bez, wert] of felder) { stammdatenListe.append( erstelleElement("dt", { text: bez }), erstelleElement("dd", { text: wert }) ); } const basisKarte = erstelleKarte(agent.displayName, [stammdatenListe], { aktionen }); // Werkzeuge-Karte const werkzeugeListe = erstelleElement("div", { klasse: "werkzeuge-liste" }); if (!agent.tools || agent.tools.length === 0) { werkzeugeListe.append( erstelleElement("p", { klasse: "gedaempft", text: t("agents.noTools") }) ); } else { for (const tool of agent.tools) { const zeile = erstelleElement("div", { klasse: "werkzeug-zeile" }); const links = erstelleElement("div", { klasse: "werkzeug-info" }); const toolTitel = erstelleElement("strong", { text: tool.name }); const toolDesc = erstelleElement("div", { klasse: "werkzeug-beschreibung gedaempft", text: tool.description || "" }); links.append(toolTitel, toolDesc); const rechts = erstelleElement("div", { klasse: "werkzeug-aktionen" }); const einstellungenBtn = erstelleElement( "button", { klasse: "schaltflaeche flach klein", title: t("agents.configTooltip"), onclick: () => zeigeWerkzeugEinstellungen(agent.agentId, tool.name) }, erstelleIcon(PFADE.einstellungen, 14), erstelleElement("span", { text: ` ${t("agents.configuration")}` }) ); const toggleLabel = erstelleElement("label", { klasse: "schalter" }); const toggleInput = erstelleElement("input", { type: "checkbox", checked: Boolean(tool.enabled), onchange: async (e) => { const aktiv = e.target.checked; try { await api( `/agents/${encodeURIComponent(agent.agentId)}/tools/${encodeURIComponent(tool.name)}?enabled=${aktiv}`, { method: "PUT" } ); tool.enabled = aktiv; zeigeMeldung( t("agents.toolToggled", { tool: tool.name, status: aktiv ? t("agents.enabled") : t("agents.disabled") }), "info" ); } catch (err) { e.target.checked = !aktiv; zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr"); } } }); const toggleSpan = erstelleElement("span", { klasse: "schalter-regler" }); toggleLabel.append(toggleInput, toggleSpan); rechts.append(einstellungenBtn, toggleLabel); zeile.append(links, rechts); werkzeugeListe.append(zeile); } } const werkzeugeKarte = erstelleKarte(t("agents.availableTools"), [werkzeugeListe]); detailContainer.append(basisKarte, werkzeugeKarte); } function oeffneNeuerAgentDialog() { const formular = erstelleElement("form", { klasse: "formular-raster", id: "neuer-agent-formular" }); const idFeld = erstelleElement("input", { type: "text", required: true, placeholder: t("agents.idPlaceholder"), id: "neu-agent-id" }); const nameFeld = erstelleElement("input", { type: "text", required: true, placeholder: t("agents.namePlaceholder"), id: "neu-agent-name" }); const modellFeld = erstelleElement("input", { type: "text", required: true, placeholder: "z.B. anthropic/claude-3.5-sonnet", value: "anthropic/claude-3.5-sonnet", id: "neu-agent-modell" }); const descFeld = erstelleElement("textarea", { rows: "3", placeholder: t("agents.descPlaceholder"), id: "neu-agent-desc" }); formular.append( erstelleElement("label", { text: t("agents.idLabel") }), idFeld, erstelleElement("label", { text: t("agents.nameLabel") }), nameFeld, erstelleElement("label", { text: t("agents.modelLabel") }), modellFeld, erstelleElement("label", { text: t("agents.descLabel") }), descFeld ); const abbrechenBtn = erstelleElement( "button", { type: "button", klasse: "schaltflaeche flach", onclick: () => modal.schliessen() }, erstelleElement("span", { text: t("common.cancel") }) ); const speichernBtn = erstelleElement( "button", { type: "submit", klasse: "schaltflaeche primaer", form: "neuer-agent-formular", onclick: () => { formular.requestSubmit(); } }, erstelleElement("span", { text: t("agents.createButton") }) ); const modal = erstelleModal(t("agents.newAgentTitle"), formular, [abbrechenBtn, speichernBtn]); formular.addEventListener("submit", async (e) => { e.preventDefault(); speichernBtn.disabled = true; try { await api("/agents", { method: "POST", body: { agentId: idFeld.value.trim(), displayName: nameFeld.value.trim(), model: modellFeld.value.trim(), description: descFeld.value.trim() } }); modal.schliessen(); zeigeMeldung(t("agents.createdSuccess", { name: nameFeld.value.trim() }), "erfolg"); await ladeUndRendereListe(idFeld.value.trim()); } catch (err) { zeigeMeldung(t("agents.createFailed", { error: err.message }), "gefahr"); } finally { speichernBtn.disabled = false; } }); modal.oeffnen(); } async function loescheAgent(agentId, name) { if (!confirm(t("agents.deleteConfirm", { name, id: agentId }))) return; try { await api(`/agents/${encodeURIComponent(agentId)}`, { method: "DELETE" }); zeigeMeldung(t("agents.deleted", { name }), "info"); await ladeUndRendereListe(); } catch (err) { zeigeMeldung(t("agents.deleteFailed", { error: err.message }), "gefahr"); } } async function zeigeWerkzeugEinstellungen(agentId, toolName) { const box = erstelleElement("div", { klasse: "einstellungen-box" }); box.append(erstelleElement("p", { klasse: "gedaempft", text: t("agents.loadingSettings") })); const schliessenBtn = erstelleElement("button", { type: "button", klasse: "schaltflaeche flach", onclick: () => modal.schliessen(), text: t("common.close") }); const modal = erstelleModal( t("agents.settingsTitle", { tool: toolName, agent: agentId }), box, [schliessenBtn] ); modal.oeffnen(); try { const daten = await api( `/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolName)}/settings` ); box.replaceChildren(); const hinweis = erstelleElement("p", { klasse: "hinweis", text: t("agents.secretHint") }); box.append(hinweis); if (!daten || Object.keys(daten).length === 0) { box.append( erstelleElement("p", { klasse: "gedaempft", text: t("agents.noSettings") }) ); } else { const dl = erstelleElement("dl", { klasse: "datenliste" }); for (const [schluessel, wert] of Object.entries(daten)) { const dt = erstelleElement("dt", { text: schluessel }); const dd = erstelleElement("dd", { text: typeof wert === "object" ? JSON.stringify(wert) : String(wert) }); dl.append(dt, dd); } box.append(dl); } } catch (err) { box.replaceChildren( erstelleElement("p", { klasse: "fehler", text: t("agents.loadFailed", { error: err.message }) }) ); } const rechteBox = erstelleElement("div", { klasse: "einstellungen-box" }); box.after(rechteBox); await zeigeWerkzeugrechte(agentId, toolName, rechteBox); } function getWirkung(effective) { const map = { auto: [t("agents.effectAuto"), "erfolg"], approve: [t("agents.effectApprove"), "warnung"], deny: [t("agents.effectDeny"), "gefahr"] }; return map[effective] ?? [effective, "neutral"]; } /** * #641: Rechte je Operation. "erlaubt" laeuft ohne Rueckfrage, "freigabe" wird einzeln * vorgelegt, "verboten" gar nicht erst. Die Wirkung zeigt, was tatsaechlich geschieht - * auch durch Dauerfreigaben oder die Vorgabe des Werkzeugs. */ async function zeigeWerkzeugrechte(agentId, toolName, box) { const pfad = `/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolName)}/rights`; let rechte; try { rechte = await api(pfad); } catch (err) { box.replaceChildren(erstelleElement("p", { klasse: "gedaempft", text: t("agents.loadFailed", { error: err.message }) })); return; } box.replaceChildren(erstelleElement("h3", { text: t("agents.rights") })); if (!rechte.operations || rechte.operations.length === 0) { box.append( erstelleElement("p", { klasse: "gedaempft", text: t("agents.noOperations") }) ); return; } box.append( erstelleElement("p", { klasse: "hinweis", text: rechte.configured ? t("agents.configuredHint") : t("agents.notConfiguredHint") }) ); const tabelle = erstelleElement("table", { klasse: "daten-tabelle rechte-tabelle" }); const kopf = erstelleElement("tr", {}); for (const titel of [t("agents.operation"), t("agents.setting"), t("agents.effect")]) { kopf.append(erstelleElement("th", { text: titel })); } tabelle.append(kopf); const auswahl = new Map(); for (const op of rechte.operations) { const select = erstelleElement( "select", { disabled: op.alwaysApproval }, erstelleElement("option", { value: "erlaubt", text: t("agents.allowed") }), erstelleElement("option", { value: "freigabe", text: t("agents.singleApproval") }), erstelleElement("option", { value: "verboten", text: t("agents.forbidden") }) ); // Ohne gesetzte Rechte die tatsaechliche Wirkung vorbelegen - sonst wuerde das erste // Speichern z.B. das freie Lesen still in eine Freigabepflicht verwandeln. select.value = rechte.configured ? op.setting : op.effective === "auto" ? "erlaubt" : op.effective === "deny" ? "verboten" : "freigabe"; auswahl.set(op.operation, select); const [wirkungText, wirkungArt] = getWirkung(op.effective); const wirkung = erstelleElement("td", {}, erstellePlakette(wirkungText, wirkungArt)); if (op.standingApprovalId) { wirkung.append(" ", erstellePlakette(t("agents.standingApproval", { id: op.standingApprovalId }), "neutral")); } wirkung.append(erstelleElement("div", { klasse: "grund", text: op.alwaysApproval ? t("agents.alwaysApprovalReason") : op.reason })); const zeile = erstelleElement("tr", {}); zeile.append(erstelleElement("td", { text: op.operation }), erstelleElement("td", {}, select), wirkung); tabelle.append(zeile); } box.append(tabelle); const speichern = erstelleElement("button", { type: "button", klasse: "schaltflaeche primaer klein", text: t("agents.saveRights"), onclick: async () => { const allowed = []; const denied = []; for (const [operation, select] of auswahl) { if (select.disabled) continue; if (select.value === "erlaubt") allowed.push(operation); if (select.value === "verboten") denied.push(operation); } speichern.disabled = true; try { const res = await api(pfad, { method: "PUT", body: { allowed, denied } }); zeigeMeldung(res?.message ?? t("agents.rightsSaved"), "erfolg"); await zeigeWerkzeugrechte(agentId, toolName, box); } catch (err) { zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr"); speichern.disabled = false; } } }); box.append(speichern); }