From 457e92584fd609f0f4496933d62cd9abc64eaccf Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Sat, 8 Aug 2026 06:48:30 +0200 Subject: [PATCH] Polish Agent Editor workflows Refine the Easy and Advanced surfaces with direct prompt editing, visible tool controls, compact transfer lists, per-row profile actions, and clearer review and removal states. Reuse the shared compact model preset selector, improve responsive layouts and copy, and extend focused WebUI contracts for the polished behavior. --- plugins/_agent_editor/AGENTS.md | 12 + .../_agent_editor/webui/agent-editor-store.js | 304 +++++++++---- plugins/_agent_editor/webui/main.html | 407 ++++++++++-------- .../model-switcher.html | 129 +++--- plugins/_model_config/webui/switcher-mixin.js | 12 +- tests/test_agent_editor.py | 20 + tests/test_agent_editor_webui.py | 246 +++++++++-- 7 files changed, 785 insertions(+), 345 deletions(-) diff --git a/plugins/_agent_editor/AGENTS.md b/plugins/_agent_editor/AGENTS.md index f0aabc42b..5eba34891 100644 --- a/plugins/_agent_editor/AGENTS.md +++ b/plugins/_agent_editor/AGENTS.md @@ -27,6 +27,18 @@ provenance and shown as higher priority; the editor still writes only the user-profile scope. - Bundled `agents/` files are read-only. +- Advanced prompt text is directly editable; per-file close/check actions + discard or accept the current edit checkpoint, while the editor's global save + remains the only persistence boundary. +- The configurable tool catalog is visible in both modes; Easy provides direct + allow/block checkboxes and points to Advanced for skill access. Skills remain + Advanced-only. Advanced keeps both complete selectors visible but disabled + for inherited access and interactive for custom access. Framework-required + tools remain absent from the tool catalog. +- Model selection reuses `_model_config`'s compact preset dropdown and preset + editor; Agent Editor persists only the scoped preset reference. +- The WebUI uses the shared modal stack, labeled prompt scroll regions, and + 24px-or-larger policy and text-action targets. ## Verification diff --git a/plugins/_agent_editor/webui/agent-editor-store.js b/plugins/_agent_editor/webui/agent-editor-store.js index 1790799a6..9307d11e8 100644 --- a/plugins/_agent_editor/webui/agent-editor-store.js +++ b/plugins/_agent_editor/webui/agent-editor-store.js @@ -41,11 +41,6 @@ function policyFromState(value, hasOverride) { return normalized; } -function easyToolMode(policy) { - if (policy.mode !== "custom") return "inherit"; - return policy.default === "block" && policy.allowed.length === 0 ? "off" : "custom"; -} - function policyAllows(policy, id) { if (policy.mode !== "custom") return true; if (policy.blocked.includes(id)) return false; @@ -81,7 +76,6 @@ const model = { draft: null, initialDraft: null, selectedPrompt: SPECIFICS, - promptGroup: "2.1", promptFileSearch: "", promptTextSearch: "", comparePrompt: "", @@ -94,10 +88,10 @@ const model = { skillOrigin: "all", selectedAllowedSkills: [], selectedBlockedSkills: [], - editingPrompts: [], - easyToolsOpen: false, + promptEditBaselines: {}, plan: { written: [], deleted: [], warnings: [] }, planLoading: false, + planStatus: "idle", pendingMutation: null, readyNoteContext: "", suppressClosePrompt: false, @@ -125,15 +119,35 @@ const model = { async mount(root) { this.revokePreview(); this.root = root; + this.draft = null; + this.initialDraft = null; + this.promptEditBaselines = {}; + this.loading = true; this.error = ""; this.pendingMutation = null; this.plan = { written: [], deleted: [], warnings: [] }; + this.planStatus = "idle"; this.view = this.intent.view === "manage" ? "manage" : "editor"; + const modalTitle = + this.view === "manage" + ? "Manage agents" + : this.intent.view === "create" + ? "Create agent" + : "Edit agent"; + this.setModalTitle(modalTitle); + const modal = this.root.closest(".modal"); + const titleAfterLoad = (event) => { + if (event.detail?.modal?.element !== modal) return; + document.removeEventListener("modal-content-loaded", titleAfterLoad); + this.setModalTitle(modalTitle); + }; + document.addEventListener("modal-content-loaded", titleAfterLoad); this.mode = "easy"; this.section = this.savedSection(); this.syncSurface(); await this.loadProfiles(); if (this.view === "manage") { + this.loading = false; this.setModalTitle("Manage agents"); return; } @@ -226,16 +240,18 @@ const model = { }; this.initialDraft = clone(this.draft); this.selectedPrompt = SPECIFICS; - this.promptGroup = "2.1"; this.comparePrompt = ""; - this.easyToolsOpen = false; + this.planStatus = "idle"; this.selectedAllowedTools = []; this.selectedBlockedTools = []; this.selectedAllowedSkills = []; this.selectedBlockedSkills = []; - this.editingPrompts = Object.values(prompts) - .filter((prompt) => prompt.has_override || prompt.filename === SPECIFICS) - .map((prompt) => prompt.filename); + this.promptEditBaselines = Object.fromEntries( + Object.values(prompts).map((prompt) => [prompt.filename, { + value: prompt.value, + reset: prompt.reset, + }]), + ); }, get dirty() { @@ -255,8 +271,8 @@ const model = { return this.draft?.prompts?.[SPECIFICS] || null; }, - get toolMode() { - return this.draft ? easyToolMode(this.draft.toolPolicy) : "inherit"; + get toolCatalog() { + return (this.state?.tools?.catalog || []).filter((item) => item.available !== false); }, get toolOrigins() { @@ -267,6 +283,10 @@ const model = { return unique((this.state?.skills?.catalog || []).map((item) => item.origin)).sort(); }, + get skillCatalog() { + return (this.state?.skills?.catalog || []).filter((item) => item.available !== false); + }, + get promptGroups() { const groups = new Map(); for (const prompt of Object.values(this.draft?.prompts || {})) { @@ -312,9 +332,10 @@ const model = { inner?.classList.toggle("agent-editor-easy", this.mode !== "advanced"); }, - setMode(mode, section = "") { + setMode(mode, section = "", preview = true) { this.mode = mode === "advanced" ? "advanced" : "easy"; - if (section) this.setSection(section); + if (section) this.setSection(section, preview); + else if (preview && this.mode === "advanced" && this.section === "5") this.previewPlan(); this.syncSurface(); if (this.mode === "advanced") { requestAnimationFrame(() => { @@ -323,12 +344,12 @@ const model = { } }, - setSection(section) { + setSection(section, preview = true) { this.section = String(section || "1"); try { localStorage.setItem(LAST_SECTION_KEY, this.section); } catch {} - if (this.section === "5") this.previewPlan(); + if (preview && this.section === "5") this.previewPlan(); }, savedSection() { @@ -354,18 +375,21 @@ const model = { return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "A"; }, - fallbackColor() { - const source = this.draft?.profileId || this.draft?.title || "agent"; - const palette = ["#6C5CE7", "#0984E3", "#00A884", "#D35400", "#C0392B", "#8E44AD"]; - let hash = 0; - for (const char of source) hash = ((hash * 31) + char.charCodeAt(0)) >>> 0; - return palette[hash % palette.length]; + profileVisual(profile = {}) { + const id = String(profile.id || profile.key || ""); + const title = String(profile.title || profile.label || id || "Agent"); + const visual = modelConfigStore.getAgentProfileVisual(id, title); + return { + ...visual, + color: profile.avatar?.kind === "color" ? profile.avatar.value : visual.color, + url: profile.avatar_url || visual.url, + }; }, avatarColor() { return this.draft?.avatar?.kind === "color" ? this.draft.avatar.value - : this.fallbackColor(); + : this.profileVisual({ id: this.draft?.profileId, title: this.draft?.title }).color; }, chooseAvatarColor(value) { @@ -443,12 +467,13 @@ const model = { metadataProvenance(key) { const metadata = this.state?.profile?.metadata?.[key] || {}; - if (this.metadataResetPending(key)) { - return `Inherited source: ${metadata.inherited_source || "none"}`; - } - return metadata.has_override - ? `Your override · ${metadata.source || "user layer"}` - : `Inherited · ${metadata.source || "no lower value"}`; + if (metadata.has_override && !this.metadataResetPending(key)) return "Customized by you"; + const match = String(metadata.inherited_source || metadata.source || "") + .match(/(?:^|\/)agents\/([^/]+)/); + const sourceId = match?.[1] || ""; + if (!sourceId || sourceId === this.state?.profile?.id) return "Using the default"; + const source = this.profiles.find((profile) => profile.id === sourceId)?.title || sourceId; + return `Inherited from ${source}`; }, markMetadataSet(key) { @@ -460,23 +485,46 @@ const model = { if (!prompt) return; prompt.value = String(prompt.inherited || ""); prompt.reset = true; + this.acceptPromptEdit(prompt.filename); }, markPromptSet(filename) { const prompt = this.draft?.prompts?.[filename]; if (prompt) { prompt.reset = false; - if (!this.editingPrompts.includes(filename)) this.editingPrompts.push(filename); + this.acceptPromptEdit(filename); } }, - isPromptEditing(filename) { - return this.editingPrompts.includes(filename); + onPromptInput(filename) { + const prompt = this.draft?.prompts?.[filename]; + if (prompt) prompt.reset = false; }, - beginPromptEdit(filename) { - if (!this.editingPrompts.includes(filename)) this.editingPrompts.push(filename); - requestAnimationFrame(() => this.root?.querySelector("#agent-editor-prompt-text")?.focus()); + promptEditPending(prompt) { + const baseline = this.promptEditBaselines[prompt?.filename]; + return Boolean( + prompt + && baseline + && (prompt.value !== baseline.value || prompt.reset !== baseline.reset), + ); + }, + + acceptPromptEdit(filename) { + const prompt = this.draft?.prompts?.[filename]; + if (!prompt) return; + this.promptEditBaselines[filename] = { + value: prompt.value, + reset: prompt.reset, + }; + }, + + discardPromptEdit(filename) { + const prompt = this.draft?.prompts?.[filename]; + const baseline = this.promptEditBaselines[filename]; + if (!prompt || !baseline) return; + prompt.value = baseline.value; + prompt.reset = baseline.reset; }, resetPrompt(filename) { @@ -484,19 +532,26 @@ const model = { if (!prompt) return; prompt.value = prompt.inherited; prompt.reset = true; - this.editingPrompts = this.editingPrompts.filter((item) => item !== filename); + this.acceptPromptEdit(filename); }, promptDisplayState(prompt) { - if (prompt?.reset) return "Reset to inherited"; - if (this.promptDirty(prompt)) return prompt.value === "" ? "Overridden here (empty)" : "Overridden here"; - return prompt?.state || "Unavailable"; + if (prompt?.reset) return "Will use the default"; + if (prompt?.has_override || this.promptDirty(prompt)) return "Customized by you"; + return prompt?.state === "Unavailable" ? "Unavailable" : "Default"; }, promptSourceChain(prompt) { - const chain = [...(prompt?.source_chain || [])].filter((item) => item !== "Your override"); - if (!prompt?.reset && (prompt?.has_override || this.promptDirty(prompt))) chain.push("Your override"); - return chain.join(" → ") || "No inherited source"; + if (!prompt?.reset && (prompt?.has_override || this.promptDirty(prompt))) return "Customized by you"; + const source = [...(prompt?.source_chain || [])] + .filter((item) => item !== "Your override") + .at(-1); + const current = String( + this.state?.profile?.metadata?.title?.effective || this.state?.profile?.id || "", + ); + return !source || source.toLowerCase() === current.toLowerCase() + ? "Default" + : `Inherited from ${source}`; }, selectPrompt(filename) { @@ -506,10 +561,10 @@ const model = { this.comparePrompt = ""; }, - filteredPromptFiles() { + filteredPromptFiles(group = "") { const query = this.promptFileSearch.trim().toLowerCase(); return Object.values(this.draft?.prompts || {}).filter((prompt) => - prompt.group === this.promptGroup && (!query || [prompt.filename, prompt.state, prompt.source] + (!group || prompt.group === group) && (!query || [prompt.filename, prompt.state, prompt.source] .join(" ").toLowerCase().includes(query)), ); }, @@ -546,12 +601,10 @@ const model = { globalThis.justToast?.("Path copied", "success", 1200, "agent-editor-copy"); }, - setEasyToolMode(mode) { - if (mode === "inherit") { - this.draft.toolPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] }; - } else if (mode === "off") { - this.draft.toolPolicy = { mode: "custom", default: "block", allowed: [], blocked: [] }; - } + useStandardTools() { + this.draft.toolPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] }; + this.selectedAllowedTools = []; + this.selectedBlockedTools = []; }, chooseTools() { @@ -561,6 +614,28 @@ const model = { this.setMode("advanced", "3"); }, + setEasyToolAllowed(id, allow) { + if (this.draft.toolPolicy.mode !== "custom") { + this.draft.toolPolicy = { mode: "custom", default: "allow", allowed: [], blocked: [] }; + } + this.moveTools([id], allow); + const policy = this.draft.toolPolicy; + if (this.initialDraft?.toolPolicy.mode !== "custom" && policy.default === "allow" + && !policy.allowed.length && !policy.blocked.length) this.useStandardTools(); + }, + + useStandardSkills() { + this.draft.skillPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] }; + this.selectedAllowedSkills = []; + this.selectedBlockedSkills = []; + }, + + chooseSkills() { + if (this.draft.skillPolicy.mode !== "custom") { + this.draft.skillPolicy = { mode: "custom", default: "allow", allowed: [], blocked: [] }; + } + }, + setPolicyDefault(kind, nextDefault) { const policy = kind === "tool" ? this.draft.toolPolicy : this.draft.skillPolicy; const catalog = kind === "tool" ? this.state.tools.catalog : this.state.skills.catalog; @@ -585,6 +660,7 @@ const model = { filteredTools(allowed) { const query = this.toolSearch.trim().toLowerCase(); return (this.state?.tools?.catalog || []).filter((item) => { + if (this.draft.toolPolicy.mode !== "custom" && item.available === false) return false; if (this.isToolAllowed(item) !== allowed) return false; const category = item.id.split(":", 1)[0]; if (this.toolCategory !== "all" && category !== this.toolCategory) return false; @@ -601,12 +677,17 @@ const model = { }, moveAllVisibleTools(allow) { - this.moveTools(this.filteredTools(!allow).map((item) => item.id), allow); + return this.confirmBulkMove( + "tool", + this.filteredTools(!allow).map((item) => item.id), + allow, + ); }, filteredSkills(allowed) { const query = this.skillSearch.trim().toLowerCase(); return (this.state?.skills?.catalog || []).filter((item) => { + if (this.draft.skillPolicy.mode !== "custom" && item.available === false) return false; if (this.isSkillAllowed(item) !== allowed) return false; if (this.skillOrigin !== "all" && item.origin !== this.skillOrigin) return false; return !query || [item.name, item.description, item.origin, ...(item.tags || [])] @@ -621,7 +702,26 @@ const model = { }, moveAllVisibleSkills(allow) { - this.moveSkills(this.filteredSkills(!allow).map((item) => item.name), allow); + return this.confirmBulkMove( + "skill", + this.filteredSkills(!allow).map((item) => item.name), + allow, + ); + }, + + async confirmBulkMove(kind, ids, allow) { + if (!ids.length) return; + const action = allow ? "Allow" : "Block"; + const plural = `${kind}${ids.length === 1 ? "" : "s"}`; + const confirmed = await showConfirmDialog({ + title: `${action} ${ids.length} shown ${plural}?`, + message: `

This changes every ${kind} currently shown by your filters.

`, + confirmText: `${action} shown ${plural}`, + type: "warning", + }); + if (!confirmed) return; + if (kind === "tool") this.moveTools(ids, allow); + else this.moveSkills(ids, allow); }, skillWarnings(skill) { @@ -647,20 +747,44 @@ const model = { } }, - validationErrors() { - const errors = []; - if (!this.draft?.title.trim()) errors.push("Agent name is required."); - if (this.draft?.creating) { - if (!this.draft.profileId || !PROFILE_ID.test(this.draft.profileId)) { - errors.push("Enter a name that produces a valid profile ID."); - } - if (this.profileConflict) errors.push(`An agent with profile ID ${this.draft.profileId} already exists.`); - if (!this.instructions?.value.trim()) errors.push("Instructions are required for a new agent."); - } else if (this.mode === "easy" && !this.instructions?.value.trim()) { - errors.push("Instructions can’t be empty. To remove your changes, use Restore original instructions."); + validationIssues() { + const issues = []; + if (!this.draft?.title.trim()) { + issues.push({ key: "name", section: "1", field: "agent-editor-advanced-name", label: "Agent name", message: "Agent name is required." }); } - if (this.avatarUploading) errors.push("Wait for the avatar upload to finish."); - return errors; + if (this.draft?.creating) { + if (this.draft.title.trim() && (!this.draft.profileId || !PROFILE_ID.test(this.draft.profileId))) { + issues.push({ key: "name", section: "1", field: "agent-editor-advanced-name", label: "Agent name", message: "Enter a name that produces a valid profile ID." }); + } + if (this.profileConflict) { + issues.push({ key: "name", section: "1", field: "agent-editor-advanced-name", label: "Agent name", message: `An agent with profile ID ${this.draft.profileId} already exists.` }); + } + if (!this.instructions?.value.trim()) { + issues.push({ key: "instructions", section: "2", field: "agent-editor-prompt-text", label: "Instructions", message: "Instructions are required for a new agent." }); + } + } + if (this.avatarUploading) { + issues.push({ key: "avatar", section: "1", field: "agent-editor-advanced-name", label: "Avatar", message: "Wait for the avatar upload to finish." }); + } + return issues; + }, + + validationErrors() { + return this.validationIssues().map((issue) => issue.message); + }, + + sectionIssues(section) { + return this.validationIssues().filter((issue) => issue.section === String(section)); + }, + + fieldIssue(key) { + return this.validationIssues().find((issue) => issue.key === key) || null; + }, + + showValidationIssue(issue) { + if (!issue) return; + if (issue.key === "instructions") this.selectPrompt(SPECIFICS); + this.setMode("advanced", issue.section); }, buildPatch() { @@ -701,12 +825,9 @@ const model = { : { mode: "inherit" }; } if (!same(this.draft.toolPolicy, this.initialDraft.toolPolicy)) { - const mode = easyToolMode(this.draft.toolPolicy); - patch.tool_policy = mode === "inherit" + patch.tool_policy = this.draft.toolPolicy.mode === "inherit" ? { mode: "inherit" } - : mode === "off" - ? { mode: "off" } - : clone(this.draft.toolPolicy); + : clone(this.draft.toolPolicy); } if (!same(this.draft.skillPolicy, this.initialDraft.skillPolicy)) { patch.skill_policy = this.draft.skillPolicy.mode === "inherit" @@ -720,11 +841,13 @@ const model = { if (!this.draft) return false; const errors = this.validationErrors(); if (errors.length) { - this.error = errors[0]; + this.error = ""; this.plan = { written: [], deleted: [], warnings: [] }; + this.planStatus = "blocked"; return false; } this.planLoading = true; + this.planStatus = "loading"; this.error = ""; this.pendingMutation = null; try { @@ -734,8 +857,10 @@ const model = { context_id: this.intent.contextId, }); this.plan = data; + this.planStatus = "ready"; return true; } catch (error) { + this.planStatus = "error"; this.error = error.message || String(error); return false; } finally { @@ -747,7 +872,7 @@ const model = { if (this.saving || !this.draft) return false; const errors = this.validationErrors(); if (errors.length) { - this.error = errors[0]; + this.error = ""; return false; } this.saving = true; @@ -829,8 +954,9 @@ const model = { context_id: this.intent.contextId, }); this.plan = data; + this.planStatus = "ready"; this.pendingMutation = { destructive }; - this.setMode("advanced", "5"); + this.setMode("advanced", "5", false); } catch (error) { this.error = error.message || String(error); } finally { @@ -840,12 +966,18 @@ const model = { async applyPendingMutation() { if (!this.pendingMutation) return; - const count = (this.plan.written?.length || 0) + (this.plan.deleted?.length || 0); + const planned = [ + ["Will update", this.plan.written || []], + ["Will delete", this.plan.deleted || []], + ].filter(([, paths]) => paths.length); + const changes = planned.length + ? planned.map(([label, paths]) => `

${label}

`).join("") + : "

No files will change.

"; const confirmed = await showConfirmDialog({ - title: this.pendingMutation.destructive ? "Delete the entire user override?" : "Remove my changes?", - message: `${count} planned file change${count === 1 ? "" : "s"}. Bundled files are not touched.`, - confirmText: "Apply", - type: this.pendingMutation.destructive ? "danger" : "warning", + title: this.pendingMutation.destructive ? "Delete all customizations for this profile?" : "Remove my changes?", + message: `${changes}

Agent Zero’s defaults are not touched.

`, + confirmText: this.pendingMutation.destructive ? "Delete planned files" : "Remove planned changes", + type: "danger", }); if (!confirmed) return; try { @@ -857,7 +989,7 @@ const model = { }); this.pendingMutation = null; await this.loadEditor(this.draft.profileId); - globalThis.justToast?.("Your agent overrides were removed.", "success", 2200); + globalThis.justToast?.("Your agent customizations were removed.", "success", 2200); } catch (error) { this.error = error.message || String(error); } @@ -872,7 +1004,7 @@ const model = { }); const confirmed = await showConfirmDialog({ title: `Delete ${escapeHtml(profileId)}?`, - message: `${this.deletionImpactHtml(data)}

This removes only the custom user profile and cannot be undone.

`, + message: `${this.deletionImpactHtml(data)}

This permanently removes this custom agent.

`, confirmText: "Delete agent", type: "danger", }); @@ -904,9 +1036,9 @@ const model = { return [ `

Files${list(impact.files || data?.deleted || [], "None")}

`, `

Model preset
${escapeHtml(impact.model_preset || "None")}

`, - `

Project references${list(impact.project_references, "None found")}

`, - `

Active sessions${list(impact.active_sessions, "None found")}

`, - `

Profile content
${escapeHtml(contents.length ? contents.join(", ") : "No tools, extensions, skills, assets, or plugin data")}

`, + `

Projects using this agent${list(impact.project_references, "None found")}

`, + `

Open chats using this agent${list(impact.active_sessions, "None found")}

`, + `

Saved settings
${escapeHtml(contents.length ? contents.join(", ") : "No additional settings or assets")}

`, ].join(""); }, diff --git a/plugins/_agent_editor/webui/main.html b/plugins/_agent_editor/webui/main.html index 7a8f512ca..062bf1694 100644 --- a/plugins/_agent_editor/webui/main.html +++ b/plugins/_agent_editor/webui/main.html @@ -25,32 +25,26 @@