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.
This commit is contained in:
Alessandro 2026-08-08 06:48:30 +02:00
parent cf7fd3d566
commit 457e92584f
7 changed files with 785 additions and 345 deletions

View file

@ -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

View file

@ -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: `<p>This changes every ${kind} currently shown by your filters.</p>`,
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 cant 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]) => `<p><strong>${label}</strong></p><ul>${paths.map((path) => `<li><code>${escapeHtml(path)}</code></li>`).join("")}</ul>`).join("")
: "<p>No files will change.</p>";
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}<p>Agent Zeros defaults are not touched.</p>`,
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)}<p>This removes only the custom user profile and cannot be undone.</p>`,
message: `${this.deletionImpactHtml(data)}<p>This permanently removes this custom agent.</p>`,
confirmText: "Delete agent",
type: "danger",
});
@ -904,9 +1036,9 @@ const model = {
return [
`<p><strong>Files</strong>${list(impact.files || data?.deleted || [], "None")}</p>`,
`<p><strong>Model preset</strong><br>${escapeHtml(impact.model_preset || "None")}</p>`,
`<p><strong>Project references</strong>${list(impact.project_references, "None found")}</p>`,
`<p><strong>Active sessions</strong>${list(impact.active_sessions, "None found")}</p>`,
`<p><strong>Profile content</strong><br>${escapeHtml(contents.length ? contents.join(", ") : "No tools, extensions, skills, assets, or plugin data")}</p>`,
`<p><strong>Projects using this agent</strong>${list(impact.project_references, "None found")}</p>`,
`<p><strong>Open chats using this agent</strong>${list(impact.active_sessions, "None found")}</p>`,
`<p><strong>Saved settings</strong><br>${escapeHtml(contents.length ? contents.join(", ") : "No additional settings or assets")}</p>`,
].join("");
},

View file

@ -25,32 +25,26 @@
</template>
<template x-if="!$store.agentEditor.loading && $store.agentEditor.view === 'manage'">
<section class="agent-manager" aria-labelledby="agent-manager-title">
<div class="agent-manager-heading">
<div>
<h2 id="agent-manager-title">Manage agents</h2>
<p>Built-in profiles stay read-only. Your edits live as sparse overrides.</p>
</div>
<button type="button" class="button primary" @click="$store.agentEditor.loadEditor('new-agent', true)">
<x-icon name="add"></x-icon><span>Create agent</span>
</button>
</div>
<section class="agent-manager" aria-label="Manage agents">
<p class="agent-manager-intro">Edits to built-in agents are saved as your own changes — originals are never modified.</p>
<div class="agent-manager-list">
<template x-for="profile in $store.agentEditor.profiles" :key="profile.id">
<article class="agent-manager-card">
<div class="agent-manager-avatar" :style="`background:${profile.avatar?.kind === 'color' ? profile.avatar.value : '#586174'}`">
<img x-show="profile.avatar_url" :src="profile.avatar_url" :alt="`${profile.title || profile.id} avatar`">
<span x-show="!profile.avatar_url" x-text="String(profile.title || profile.id).split(/\s+/).slice(0,2).map(word => word[0]).join('').toUpperCase()"></span>
<div class="agent-manager-avatar" :style="`background:${$store.agentEditor.profileVisual(profile).color}`">
<img x-show="$store.agentEditor.profileVisual(profile).url" :src="$store.agentEditor.profileVisual(profile).url" :alt="`${profile.title || profile.id} avatar`">
<span x-show="!$store.agentEditor.profileVisual(profile).url" x-text="$store.agentEditor.profileVisual(profile).initials"></span>
</div>
<div class="agent-manager-copy">
<div class="agent-manager-name">
<strong x-text="profile.title || profile.id"></strong>
<span class="agent-origin" x-text="profile.origin"></span>
<span class="agent-change-dot" x-show="profile.has_user_overrides" title="Includes your changes" aria-label="Includes your changes"></span>
<span class="agent-status-badge is-customized" x-show="profile.has_user_overrides">
<x-icon name="edit_note"></x-icon><span>Customized by you</span>
</span>
</div>
<p x-text="profile.description || 'No description'"></p>
<code x-text="profile.id"></code>
<div class="agent-project-notice" x-show="profile.project_override_active">Project override is currently higher priority.</div>
<div class="agent-project-notice" x-show="profile.project_override_active">This projects customization takes priority here.</div>
</div>
<div class="agent-manager-actions">
<button type="button" class="button" @click="$store.agentEditor.loadEditor(profile.id, false)"><x-icon name="edit"></x-icon>Edit</button>
@ -65,11 +59,10 @@
<template x-if="!$store.agentEditor.loading && $store.agentEditor.view === 'editor' && $store.agentEditor.draft">
<div class="agent-editor-workspace">
<header class="agent-editor-topbar">
<div class="agent-editor-heading">
<div class="agent-editor-heading" x-show="$store.agentEditor.intent.view === 'manage' || (!$store.agentEditor.draft.creating && $store.agentEditor.dirty)">
<button type="button" class="button icon" x-show="$store.agentEditor.intent.view === 'manage'" aria-label="Back to agents" @click="$store.agentEditor.showManager()"><x-icon name="arrow_back"></x-icon></button>
<div>
<h2 x-text="$store.agentEditor.title"></h2>
<div class="agent-editor-subtitle" x-show="$store.agentEditor.dirty"><span class="dirty-dot"></span> Unsaved changes</div>
<div class="agent-editor-subtitle" x-show="$store.agentEditor.dirty">
<span class="agent-status-badge is-unsaved"><x-icon name="edit_note"></x-icon><span>Unsaved changes</span></span>
</div>
</div>
<div class="agent-mode-switch" role="group" aria-label="Editor mode">
@ -87,78 +80,64 @@
<div class="avatar-progress" x-show="$store.agentEditor.avatarUploading" aria-label="Uploading avatar"><x-icon class="spinning" name="progress_activity"></x-icon></div>
</div>
<div class="agent-avatar-actions">
<label class="avatar-color-action">Choose color <input type="color" :value="$store.agentEditor.avatarColor()" @input="$store.agentEditor.chooseAvatarColor($event.target.value)" aria-label="Choose avatar color"></label>
<label class="text-button avatar-upload-action">Upload image <input type="file" accept="image/png,image/jpeg,image/webp" @change="$store.agentEditor.uploadAvatar($event)" aria-label="Upload avatar image"></label>
<label class="avatar-action-icon avatar-color-action" title="Choose color"><x-icon name="palette"></x-icon><span class="sr-only">Choose color</span><input type="color" :value="$store.agentEditor.avatarColor()" @input="$store.agentEditor.chooseAvatarColor($event.target.value)" aria-label="Choose avatar color"></label>
<label class="avatar-action-icon avatar-upload-action" title="Upload image"><x-icon name="add_photo_alternate"></x-icon><span class="sr-only">Upload image</span><input type="file" accept="image/png,image/jpeg,image/webp" @change="$store.agentEditor.uploadAvatar($event)" aria-label="Upload avatar image"></label>
<button type="button" class="text-button" x-show="$store.agentEditor.state.profile.metadata.avatar.has_override || $store.agentEditor.draft.avatar" @click="$store.agentEditor.resetAvatar()">Remove</button>
</div>
</div>
<label class="agent-field agent-name-field">
<span class="agent-field-label">Agent name</span>
<input id="agent-editor-name" type="text" x-model="$store.agentEditor.draft.title" @input="$store.agentEditor.onNameInput(); $store.agentEditor.markMetadataSet('title')" required autocomplete="off">
<input id="agent-editor-name" type="text" x-model="$store.agentEditor.draft.title" @input="$store.agentEditor.onNameInput(); $store.agentEditor.markMetadataSet('title')" required autocomplete="off" :aria-invalid="$store.agentEditor.fieldIssue('name') ? 'true' : null" :aria-describedby="$store.agentEditor.fieldIssue('name') ? 'agent-editor-name-error' : null">
</label>
<button type="button" class="agent-advanced-link identity-link" @click="$store.agentEditor.setMode('advanced', '1')">Advanced <span aria-hidden="true"></span></button>
<div class="agent-id-feedback" x-show="$store.agentEditor.draft.creating && $store.agentEditor.draft.title && (!$store.agentEditor.draft.profileId || $store.agentEditor.profileConflict)">
<template x-if="!$store.agentEditor.draft.profileId">
<span class="field-error">This name cannot produce a supported profile ID.</span>
</template>
<template x-if="$store.agentEditor.profileConflict">
<span class="field-error">An agent with profile ID <code x-text="$store.agentEditor.draft.profileId"></code> already exists. <button type="button" class="text-button" @click="$store.agentEditor.openConflictingProfile()">Open it</button></span>
</template>
<div class="agent-id-feedback" x-show="$store.agentEditor.fieldIssue('name')">
<span id="agent-editor-name-error" class="field-error" role="alert" x-text="$store.agentEditor.fieldIssue('name')?.message"></span>
<button type="button" class="text-button" x-show="$store.agentEditor.profileConflict" @click="$store.agentEditor.openConflictingProfile()">Open existing agent</button>
</div>
</section>
<section class="agent-easy-field">
<div class="agent-field-heading">
<div><label for="agent-editor-instructions" class="agent-field-label">Instructions</label><p>What should this agent do, and how should it behave?</p></div>
<button type="button" class="agent-advanced-link" @click="$store.agentEditor.setMode('advanced', '2')">Advanced <span aria-hidden="true"></span></button>
</div>
<textarea id="agent-editor-instructions" rows="9" x-model="$store.agentEditor.instructions.value" @input="$store.agentEditor.markPromptSet('agent.system.main.specifics.md')" placeholder="Research technical topics, verify important claims with reliable sources, and return concise reports with links."></textarea>
<button type="button" class="text-button restore-action" x-show="$store.agentEditor.instructions.has_override && !$store.agentEditor.instructions.reset" @click="$store.agentEditor.restoreInstructions()"><x-icon name="restart_alt"></x-icon>Restore original instructions</button>
<div>
<textarea id="agent-editor-instructions" rows="9" x-model="$store.agentEditor.instructions.value" @input="$store.agentEditor.markPromptSet('agent.system.main.specifics.md')" placeholder="Research technical topics, verify important claims with reliable sources, and return concise reports with links." :aria-invalid="$store.agentEditor.fieldIssue('instructions') ? 'true' : null" :aria-describedby="$store.agentEditor.fieldIssue('instructions') ? 'agent-editor-instructions-error' : null"></textarea>
<span id="agent-editor-instructions-error" class="field-error" role="alert" x-show="$store.agentEditor.fieldIssue('instructions')" x-text="$store.agentEditor.fieldIssue('instructions')?.message"></span>
<button type="button" class="text-button restore-action" x-show="$store.agentEditor.instructions.has_override && !$store.agentEditor.instructions.reset" @click="$store.agentEditor.restoreInstructions()"><x-icon name="restart_alt"></x-icon>Use default instructions</button>
</div>
</section>
<section class="agent-easy-field agent-easy-tools">
<div class="agent-field-heading">
<div><div class="agent-field-label">Tools</div></div>
<button type="button" class="agent-advanced-link" @click="$store.agentEditor.setMode('advanced', '3')">Advanced <span aria-hidden="true"></span></button>
<div><div class="agent-field-label">Tools</div><p>Choose which tools this agent can use.</p></div>
</div>
<div class="easy-tool-summary" role="status" :aria-label="$store.agentEditor.toolMode === 'inherit' ? 'Standard tools' : $store.agentEditor.toolMode === 'off' ? 'No optional tools' : 'Custom selection'">
<div class="tool-state-icon" aria-hidden="true" x-text="$store.agentEditor.toolMode === 'inherit' ? '●' : $store.agentEditor.toolMode === 'off' ? '○' : '◐'"></div>
<div class="easy-tool-copy">
<strong x-text="$store.agentEditor.toolMode === 'inherit' ? 'Standard tools — recommended' : $store.agentEditor.toolMode === 'off' ? 'No optional tools' : 'Custom selection'"></strong>
<span x-text="$store.agentEditor.toolMode === 'inherit' ? 'This agent can use Agent Zeros standard tools.' : $store.agentEditor.toolMode === 'off' ? 'Optional tools are off for this agent.' : 'This agent uses a custom set of tools.'"></span>
</div>
<button type="button" class="button" x-show="$store.agentEditor.toolMode !== 'custom'" @click="$store.agentEditor.easyToolsOpen = !$store.agentEditor.easyToolsOpen">Change</button>
<button type="button" class="button" x-show="$store.agentEditor.toolMode === 'custom'" @click="$store.agentEditor.setMode('advanced', '3')">Edit in Advanced</button>
<div class="easy-tool-list" role="list" aria-label="Tools this agent can use">
<template x-for="tool in $store.agentEditor.toolCatalog" :key="tool.id">
<label class="policy-item" role="listitem"><input type="checkbox" :checked="$store.agentEditor.isToolAllowed(tool)" :aria-label="`Allow ${tool.label}`" @change="$store.agentEditor.setEasyToolAllowed(tool.id, $event.target.checked)"><div><strong x-text="tool.label"></strong><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p></div></label>
</template>
<p class="policy-empty" x-show="!$store.agentEditor.toolCatalog.length">No configurable tools are available.</p>
</div>
<div class="easy-tool-choices" x-show="$store.agentEditor.easyToolsOpen && $store.agentEditor.toolMode !== 'custom'">
<label><input type="radio" name="easy-tool-mode" value="inherit" :checked="$store.agentEditor.toolMode === 'inherit'" @change="$store.agentEditor.setEasyToolMode('inherit')"> <span><strong>Standard tools</strong><small>Inherit Agent Zeros standard access.</small></span></label>
<label><input type="radio" name="easy-tool-mode" value="off" :checked="$store.agentEditor.toolMode === 'off'" @change="$store.agentEditor.setEasyToolMode('off')"> <span><strong>No optional tools</strong><small>Keep only capabilities required for a valid response.</small></span></label>
<button type="button" class="text-button" @click="$store.agentEditor.chooseTools()">Choose specific tools in Advanced</button>
</div>
<div class="custom-tool-reset" x-show="$store.agentEditor.toolMode === 'custom'">
<span>This removes your custom tool selection.</span>
<button type="button" class="text-button" @click="$store.agentEditor.setEasyToolMode('inherit')">Reset to standard</button>
</div>
<div class="agent-project-notice" x-show="$store.agentEditor.state.tools.project_override_active">This project has a higher-priority tool policy. Your change will apply outside this project and wherever no project override exists.</div>
<p class="easy-skills-hint">To enable or disable skills, click Advanced.</p>
<div class="agent-project-notice" x-show="$store.agentEditor.state.tools.project_override_active">This project has different tool settings. Your changes apply wherever a project has not chosen its own settings.</div>
</section>
</main>
<div class="agent-advanced" x-show="$store.agentEditor.mode === 'advanced'">
<nav class="agent-advanced-nav" aria-label="Advanced editor sections">
<template x-for="item in [{id:'1',label:'Identity & models'},{id:'2',label:'Prompt files'},{id:'3',label:'Tools'},{id:'4',label:'Skills'},{id:'5',label:'Review & test'}]" :key="item.id">
<template x-for="item in [{id:'1',label:'Identity & models'},{id:'2',label:'Prompt files'},{id:'3',label:'Tools'},{id:'4',label:'Skills'},{id:'5',label:'Review'}]" :key="item.id">
<button type="button" :class="{ active: $store.agentEditor.section === item.id }" :aria-current="$store.agentEditor.section === item.id ? 'step' : null" @click="$store.agentEditor.setSection(item.id)">
<span class="section-number" x-text="item.id"></span><span x-text="item.label"></span><span class="section-dirty" x-show="$store.agentEditor.sectionDirty(item.id)" aria-label="Unsaved changes"></span>
<span class="section-number" x-text="item.id"></span><span x-text="item.label"></span>
<span class="agent-status-badge compact" :class="$store.agentEditor.sectionIssues(item.id).length ? 'is-error' : 'is-unsaved'" x-show="$store.agentEditor.sectionIssues(item.id).length || $store.agentEditor.sectionDirty(item.id)"><x-icon :name="$store.agentEditor.sectionIssues(item.id).length ? 'error' : 'edit_note'"></x-icon><span x-text="$store.agentEditor.sectionIssues(item.id).length ? 'Needs attention' : 'Changed'"></span></span>
</button>
</template>
</nav>
<div class="agent-advanced-content">
<section x-show="$store.agentEditor.section === '1'" data-agent-editor-section="1" tabindex="-1" aria-labelledby="agent-section-1-title">
<div class="advanced-section-heading"><div><span>Section 1</span><h3 id="agent-section-1-title">Identity & models</h3></div><p>Identity is authored in <code>agent.yaml</code>; model selection stays in the existing preset owner.</p></div>
<header class="advanced-section-heading"><h3 id="agent-section-1-title">Identity & models</h3><p>Set the agents identity, delegation guidance, and model preset.</p></header>
<div class="origin-row">
<span class="agent-origin" x-text="$store.agentEditor.state.profile.origin"></span>
<span class="agent-change-dot" x-show="$store.agentEditor.state.profile.has_user_overrides" title="Includes your changes">● Includes your changes</span>
<span class="agent-project-notice" x-show="$store.agentEditor.state.profile.project_override_active">Project override is currently higher priority.</span>
<span class="agent-status-badge is-customized" x-show="$store.agentEditor.state.profile.has_user_overrides"><x-icon name="edit_note"></x-icon><span>Customized by you</span></span>
<span class="agent-project-notice" x-show="$store.agentEditor.state.profile.project_override_active">This projects customization takes priority here.</span>
</div>
<p class="built-in-note" x-show="$store.agentEditor.state.profile.built_in">Your changes override the built-in profile. The original files stay unchanged.</p>
<div class="advanced-identity-grid">
@ -168,107 +147,130 @@
<span x-show="$store.agentEditor.draft.avatar?.kind !== 'image' || !$store.agentEditor.draft.avatarPreview" x-text="$store.agentEditor.initials()"></span>
</div>
<div class="agent-avatar-actions">
<label class="avatar-color-action">Color <input type="color" :value="$store.agentEditor.avatarColor()" @input="$store.agentEditor.chooseAvatarColor($event.target.value)" aria-label="Choose avatar color"></label>
<label class="text-button avatar-upload-action">Upload <input type="file" accept="image/png,image/jpeg,image/webp" @change="$store.agentEditor.uploadAvatar($event)" aria-label="Upload avatar image"></label>
<label class="avatar-action-icon avatar-color-action" title="Choose color"><x-icon name="palette"></x-icon><span class="sr-only">Choose color</span><input type="color" :value="$store.agentEditor.avatarColor()" @input="$store.agentEditor.chooseAvatarColor($event.target.value)" aria-label="Choose avatar color"></label>
<label class="avatar-action-icon avatar-upload-action" title="Upload image"><x-icon name="add_photo_alternate"></x-icon><span class="sr-only">Upload image</span><input type="file" accept="image/png,image/jpeg,image/webp" @change="$store.agentEditor.uploadAvatar($event)" aria-label="Upload avatar image"></label>
<button type="button" class="text-button" @click="$store.agentEditor.resetAvatar()">Reset</button>
</div>
</div>
<div class="identity-fields">
<div class="agent-field"><label for="agent-editor-advanced-name" class="agent-field-label">Agent name</label><input id="agent-editor-advanced-name" type="text" x-model="$store.agentEditor.draft.title" @input="$store.agentEditor.onNameInput(); $store.agentEditor.markMetadataSet('title')"><small class="field-provenance" x-text="$store.agentEditor.metadataProvenance('title')"></small><button type="button" class="text-button" x-show="$store.agentEditor.canResetMetadata('title')" @click="$store.agentEditor.resetMetadata('title')">Reset to inherited</button><small class="field-status" x-show="$store.agentEditor.metadataResetPending('title')">Will reset to inherited on save.</small></div>
<div class="agent-field"><label for="agent-editor-advanced-name" class="agent-field-label">Agent name</label><input id="agent-editor-advanced-name" type="text" x-model="$store.agentEditor.draft.title" @input="$store.agentEditor.onNameInput(); $store.agentEditor.markMetadataSet('title')" :aria-invalid="$store.agentEditor.fieldIssue('name') ? 'true' : null" :aria-describedby="$store.agentEditor.fieldIssue('name') ? 'agent-editor-advanced-name-error' : null"><span id="agent-editor-advanced-name-error" class="field-error" role="alert" x-show="$store.agentEditor.fieldIssue('name')" x-text="$store.agentEditor.fieldIssue('name')?.message"></span><small class="field-provenance" x-text="$store.agentEditor.metadataProvenance('title')"></small><button type="button" class="text-button" x-show="$store.agentEditor.canResetMetadata('title')" @click="$store.agentEditor.resetMetadata('title')">Reset to inherited</button><small class="field-status" x-show="$store.agentEditor.metadataResetPending('title')">Will reset to inherited on save.</small></div>
<label class="agent-field"><span class="agent-field-label">Profile ID</span><input type="text" :value="$store.agentEditor.draft.profileId" readonly aria-describedby="profile-id-help"><small id="profile-id-help">Used as the profile folder name. Existing IDs do not change when the display name changes.</small></label>
<div class="agent-field"><label for="agent-editor-description" class="agent-field-label">Description</label><input id="agent-editor-description" type="text" x-model="$store.agentEditor.draft.description" @input="$store.agentEditor.markMetadataSet('description')"><small>Short summary shown in profile lists.</small><small class="field-provenance" x-text="$store.agentEditor.metadataProvenance('description')"></small><button type="button" class="text-button" x-show="$store.agentEditor.canResetMetadata('description')" @click="$store.agentEditor.resetMetadata('description')">Reset to inherited</button><small class="field-status" x-show="$store.agentEditor.metadataResetPending('description')">Will reset to inherited on save.</small></div>
<div class="agent-field"><label for="agent-editor-context" class="agent-field-label">When should other agents use this agent?</label><textarea id="agent-editor-context" rows="3" x-model="$store.agentEditor.draft.context" @input="$store.agentEditor.markMetadataSet('context')"></textarea><small>Helps Agent Zero decide when to delegate work to this profile.</small><small class="field-provenance" x-text="$store.agentEditor.metadataProvenance('context')"></small><button type="button" class="text-button" x-show="$store.agentEditor.canResetMetadata('context')" @click="$store.agentEditor.resetMetadata('context')">Reset to inherited</button><small class="field-status" x-show="$store.agentEditor.metadataResetPending('context')">Will reset to inherited on save.</small><span class="field-status" x-show="!$store.agentEditor.draft.context">Delegation quality can be lower while this is empty.</span></div>
<div class="agent-field wide"><label for="agent-editor-description" class="agent-field-label">Description</label><textarea id="agent-editor-description" rows="2" x-model="$store.agentEditor.draft.description" @input="$store.agentEditor.markMetadataSet('description')"></textarea><small>Short summary shown in profile lists.</small><small class="field-provenance" x-text="$store.agentEditor.metadataProvenance('description')"></small><button type="button" class="text-button" x-show="$store.agentEditor.canResetMetadata('description')" @click="$store.agentEditor.resetMetadata('description')">Reset to inherited</button><small class="field-status" x-show="$store.agentEditor.metadataResetPending('description')">Will reset to inherited on save.</small></div>
<div class="agent-field wide"><label for="agent-editor-context" class="agent-field-label">When should other agents use this agent?</label><textarea id="agent-editor-context" rows="3" x-model="$store.agentEditor.draft.context" @input="$store.agentEditor.markMetadataSet('context')"></textarea><small>Helps Agent Zero decide when to delegate work to this profile.</small><small class="field-provenance" x-text="$store.agentEditor.metadataProvenance('context')"></small><button type="button" class="text-button" x-show="$store.agentEditor.canResetMetadata('context')" @click="$store.agentEditor.resetMetadata('context')">Reset to inherited</button><small class="field-status" x-show="$store.agentEditor.metadataResetPending('context')">Will reset to inherited on save.</small><span class="field-status" x-show="!$store.agentEditor.draft.context">Delegation quality can be lower while this is empty.</span></div>
</div>
</div>
<div class="model-preset-block">
<div class="agent-field-heading"><div><div class="agent-field-label">Model preset</div><p>Inherit the current scoped preset or choose an existing global setup.</p></div><button type="button" class="text-button" @click="$store.agentEditor.openPresetManager()">Manage presets</button></div>
<label class="model-preset-row"><input type="radio" name="agent-model-preset" value="" x-model="$store.agentEditor.draft.modelPreset"><span><strong>Inherit</strong><small x-text="`Effective now: ${$store.agentEditor.state.model_preset.effective}`"></small></span></label>
<template x-for="preset in $store.agentEditor.state.model_presets" :key="preset.name">
<label class="model-preset-row"><input type="radio" name="agent-model-preset" :value="preset.name" x-model="$store.agentEditor.draft.modelPreset"><span><strong x-text="preset.name"></strong><small><b>Main</b> <span x-text="`${preset.main.provider} / ${preset.main.name}`"></span> · <b>Utility</b> <span x-text="`${preset.utility.provider} / ${preset.utility.name}`"></span> · <b>Embedding</b> <span x-text="`${preset.embedding.provider} / ${preset.embedding.name}`"></span></small></span></label>
</template>
<div class="agent-model-preset">
<label class="agent-model-preset-picker">
<span><span class="agent-field-label">Model preset</span><small>Use the current preset or choose another setup for this agent.</small></span>
<select x-model="$store.agentEditor.draft.modelPreset">
<option value="" x-text="`Use current preset (${$store.agentEditor.state.model_preset.effective})`"></option>
<template x-for="preset in $store.agentEditor.state.model_presets" :key="preset.name">
<option :value="preset.name" x-text="preset.name"></option>
</template>
</select>
</label>
<button type="button" class="button" @click="$store.agentEditor.openPresetManager()"><x-icon class="icon" name="tune"></x-icon>Edit Presets</button>
</div>
</section>
<section x-show="$store.agentEditor.section === '2'" data-agent-editor-section="2" tabindex="-1" aria-labelledby="agent-section-2-title">
<div class="advanced-section-heading"><div><span>Section 2</span><h3 id="agent-section-2-title">Prompt files</h3></div><p>Edit actual same-name Markdown overrides with their source chain visible.</p></div>
<header class="advanced-section-heading"><h3 id="agent-section-2-title">Prompt files</h3><p>Customize this agents prompt files. You always see the default next to your version.</p></header>
<div class="prompt-workspace">
<aside class="prompt-browser">
<div class="prompt-groups" role="tablist" aria-label="Prompt groups">
<template x-for="group in $store.agentEditor.promptGroups" :key="group.id"><button type="button" role="tab" :aria-selected="$store.agentEditor.promptGroup === group.id" :class="{ active: $store.agentEditor.promptGroup === group.id }" @click="$store.agentEditor.promptGroup = group.id" x-text="`${group.id} ${group.label}`"></button></template>
</div>
<label class="compact-search"><span class="sr-only">Search prompt files</span><x-icon name="search"></x-icon><input type="search" x-model="$store.agentEditor.promptFileSearch" placeholder="Search files"></label>
<div class="prompt-file-list">
<template x-for="prompt in $store.agentEditor.filteredPromptFiles()" :key="prompt.filename">
<button type="button" :class="{ active: $store.agentEditor.selectedPrompt === prompt.filename }" @click="$store.agentEditor.selectPrompt(prompt.filename)">
<span class="prompt-file-name" x-text="prompt.filename"></span><span class="prompt-file-state" x-text="$store.agentEditor.promptDisplayState(prompt)"></span><span class="section-dirty" x-show="$store.agentEditor.promptDirty(prompt)" aria-label="Unsaved changes"></span>
</button>
<div class="prompt-file-list" role="region" aria-label="Prompt file list" tabindex="0">
<template x-for="group in $store.agentEditor.promptGroups" :key="group.id">
<section class="prompt-file-group" x-show="$store.agentEditor.filteredPromptFiles(group.id).length">
<h4 x-text="group.label"></h4>
<template x-for="prompt in $store.agentEditor.filteredPromptFiles(group.id)" :key="prompt.filename">
<button type="button" :class="{ active: $store.agentEditor.selectedPrompt === prompt.filename }" @click="$store.agentEditor.selectPrompt(prompt.filename)">
<span class="prompt-file-name" x-text="prompt.filename"></span><span class="prompt-file-state" x-text="$store.agentEditor.promptDisplayState(prompt)"></span>
</button>
</template>
</section>
</template>
<p class="prompt-empty" x-show="!$store.agentEditor.filteredPromptFiles().length">No prompt files match your search.</p>
</div>
</aside>
<div class="prompt-editor" x-show="$store.agentEditor.selectedPromptDraft">
<div class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0" x-show="$store.agentEditor.selectedPromptDraft">
<div class="prompt-editor-header">
<div><strong x-text="$store.agentEditor.selectedPrompt"></strong><div class="source-chain" x-text="$store.agentEditor.promptSourceChain($store.agentEditor.selectedPromptDraft)"></div></div>
<div class="prompt-actions">
<button type="button" class="button" @click="$store.agentEditor.comparePrompt = 'inherited'">View inherited</button>
<button type="button" class="button" x-show="!$store.agentEditor.isPromptEditing($store.agentEditor.selectedPrompt)" @click="$store.agentEditor.beginPromptEdit($store.agentEditor.selectedPrompt)">Edit / Create override</button>
<button type="button" class="button" @click="$store.agentEditor.comparePrompt = 'compare'">Compare</button>
<button type="button" class="button" x-show="$store.agentEditor.selectedPromptDraft.has_override || $store.agentEditor.promptDirty($store.agentEditor.selectedPromptDraft)" @click="$store.agentEditor.resetPrompt($store.agentEditor.selectedPrompt)">Reset to inherited</button>
<button type="button" class="button icon" title="Copy user override path" aria-label="Copy user override path" @click="$store.agentEditor.copyPromptPath()"><x-icon name="content_copy"></x-icon></button>
<button type="button" class="btn btn-action-header cancel" x-show="$store.agentEditor.promptEditPending($store.agentEditor.selectedPromptDraft)" title="Discard current edit" aria-label="Discard current edit" @click="$store.agentEditor.discardPromptEdit($store.agentEditor.selectedPrompt)"><x-icon name="close"></x-icon></button>
<button type="button" class="btn btn-action-header confirm" x-show="$store.agentEditor.promptEditPending($store.agentEditor.selectedPromptDraft)" title="Accept current edit" aria-label="Accept current edit" @click="$store.agentEditor.acceptPromptEdit($store.agentEditor.selectedPrompt)"><x-icon name="check"></x-icon></button>
<button type="button" class="button" x-show="$store.agentEditor.selectedPromptDraft.has_override || $store.agentEditor.promptDirty($store.agentEditor.selectedPromptDraft)" @click="$store.agentEditor.resetPrompt($store.agentEditor.selectedPrompt)">Use default</button>
<button type="button" class="button icon" title="Copy customization path" aria-label="Copy customization path" @click="$store.agentEditor.copyPromptPath()"><x-icon name="content_copy"></x-icon></button>
</div>
</div>
<div class="agent-project-notice" x-show="$store.agentEditor.selectedPromptDraft.project_override_active">This project has a higher-priority override for this file. Your change will apply outside that project and wherever no project override exists.</div>
<div class="agent-project-notice" x-show="$store.agentEditor.selectedPromptDraft.project_override_active">This project has its own version of this file. Your customization applies wherever a project has not supplied one.</div>
<div class="agent-project-notice" x-show="$store.agentEditor.selectedPromptDraft.dynamic_processor">This file is generated dynamically at runtime. Python processors are read-only and are not executed by the editor.</div>
<div class="prompt-view-tabs" role="tablist" aria-label="Prompt view">
<button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === ''" :class="{ active: $store.agentEditor.comparePrompt === '' }" @click="$store.agentEditor.comparePrompt = ''">Your version</button>
<button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === 'inherited'" :class="{ active: $store.agentEditor.comparePrompt === 'inherited' }" @click="$store.agentEditor.comparePrompt = 'inherited'">Default</button>
<button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === 'compare'" :class="{ active: $store.agentEditor.comparePrompt === 'compare' }" @click="$store.agentEditor.comparePrompt = 'compare'">Compare</button>
</div>
<div class="prompt-find"><label><span class="sr-only">Search within prompt</span><input type="search" x-model="$store.agentEditor.promptTextSearch" placeholder="Find in file" @keydown.enter.prevent="$store.agentEditor.findInPrompt($event.shiftKey ? -1 : 1)"></label><span x-text="`${$store.agentEditor.promptMatchCount()} matches`"></span><button type="button" class="button icon" aria-label="Previous match" @click="$store.agentEditor.findInPrompt(-1)"><x-icon name="keyboard_arrow_up"></x-icon></button><button type="button" class="button icon" aria-label="Next match" @click="$store.agentEditor.findInPrompt(1)"><x-icon name="keyboard_arrow_down"></x-icon></button></div>
<div class="prompt-panes" :class="{ compare: $store.agentEditor.comparePrompt === 'compare' }">
<div class="prompt-pane" x-show="$store.agentEditor.comparePrompt !== 'inherited'"><label for="agent-editor-prompt-text">Your override</label><textarea id="agent-editor-prompt-text" spellcheck="false" x-model="$store.agentEditor.selectedPromptDraft.value" :readonly="!$store.agentEditor.isPromptEditing($store.agentEditor.selectedPrompt)" @input="$store.agentEditor.markPromptSet($store.agentEditor.selectedPrompt)" aria-label="Prompt Markdown"></textarea></div>
<div class="prompt-pane inherited" x-show="$store.agentEditor.comparePrompt"><div class="prompt-pane-title">Inherited source</div><pre x-text="$store.agentEditor.selectedPromptDraft.inherited || '(empty)'" tabindex="0"></pre></div>
<div class="prompt-pane" x-show="$store.agentEditor.comparePrompt !== 'inherited'"><label for="agent-editor-prompt-text">Your version</label><textarea id="agent-editor-prompt-text" spellcheck="false" x-model="$store.agentEditor.selectedPromptDraft.value" @input="$store.agentEditor.onPromptInput($store.agentEditor.selectedPrompt)" aria-label="Prompt Markdown" :aria-invalid="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions') ? 'true' : null" :aria-describedby="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions') ? 'agent-editor-prompt-error' : null"></textarea><span id="agent-editor-prompt-error" class="field-error" role="alert" x-show="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions')" x-text="$store.agentEditor.fieldIssue('instructions')?.message"></span></div>
<div class="prompt-pane inherited" x-show="$store.agentEditor.comparePrompt"><div class="prompt-pane-title">Default</div><pre x-text="$store.agentEditor.selectedPromptDraft.inherited || '(empty)'" tabindex="0"></pre></div>
</div>
<details class="effective-preview"><summary>Effective source preview</summary><p>Static source preview only. Runtime variables, projects, skills, tools, secrets, time, and dynamic extensions can differ.</p><pre x-text="$store.agentEditor.selectedPromptDraft.preview || '(empty)'" tabindex="0"></pre></details>
<details class="effective-preview"><summary>Preview combined prompt</summary><p>Static preview only. Runtime variables, projects, skills, tools, secrets, time, and dynamic extensions can differ.</p><pre x-text="$store.agentEditor.selectedPromptDraft.preview || '(empty)'" tabindex="0"></pre></details>
</div>
</div>
</section>
<section x-show="$store.agentEditor.section === '3'" data-agent-editor-section="3" tabindex="-1" aria-labelledby="agent-section-3-title">
<div class="advanced-section-heading"><div><span>Section 3</span><h3 id="agent-section-3-title">Tools</h3></div><p>The same policy is enforced in prompts, schemas, local execution, MCP invocation, and delegated agents.</p></div>
<div class="agent-project-notice" x-show="$store.agentEditor.state.tools.project_override_active">This project has a higher-priority tool policy. Your change will apply outside this project and wherever no project override exists.</div>
<div class="policy-mode-row"><label><input type="radio" name="tool-policy-mode" :checked="$store.agentEditor.draft.toolPolicy.mode === 'inherit'" @change="$store.agentEditor.draft.toolPolicy = {mode:'inherit',default:'allow',allowed:[],blocked:[]}">Use standard tool access</label><label><input type="radio" name="tool-policy-mode" :checked="$store.agentEditor.draft.toolPolicy.mode === 'custom'" @change="$store.agentEditor.chooseTools()">Choose tools</label></div>
<template x-if="$store.agentEditor.draft.toolPolicy.mode === 'custom'"><div>
<div class="future-default"><strong>New tools are:</strong><label><input type="radio" name="tool-future-default" value="allow" :checked="$store.agentEditor.draft.toolPolicy.default === 'allow'" @change="$store.agentEditor.setPolicyDefault('tool','allow')">Allowed</label><label><input type="radio" name="tool-future-default" value="block" :checked="$store.agentEditor.draft.toolPolicy.default === 'block'" @change="$store.agentEditor.setPolicyDefault('tool','block')">Blocked</label></div>
<div class="policy-filters"><label><span class="sr-only">Search tools</span><input type="search" x-model="$store.agentEditor.toolSearch" placeholder="Search tools"></label><label>Category <select x-model="$store.agentEditor.toolCategory"><option value="all">All</option><option value="local">Local</option><option value="plugin">Plugin</option><option value="mcp">MCP</option></select></label><label>Origin <select x-model="$store.agentEditor.toolOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.toolOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
<header class="advanced-section-heading"><h3 id="agent-section-3-title">Tools</h3><p>Choose which tools this agent can use.</p></header>
<div class="agent-project-notice" x-show="$store.agentEditor.state.tools.project_override_active">This project has different tool settings. Your changes apply wherever a project has not chosen its own settings.</div>
<div class="policy-mode-row"><label><input type="radio" name="tool-policy-mode" :checked="$store.agentEditor.draft.toolPolicy.mode === 'inherit'" @change="$store.agentEditor.useStandardTools()"><span>Use standard tool access <small x-text="`(${$store.agentEditor.toolCatalog.length} tools)`"></small></span></label><label><input type="radio" name="tool-policy-mode" :checked="$store.agentEditor.draft.toolPolicy.mode === 'custom'" @change="$store.agentEditor.chooseTools()">Choose tools</label></div>
<fieldset class="policy-editor" :disabled="$store.agentEditor.draft.toolPolicy.mode !== 'custom'">
<legend class="sr-only">Tool access selection</legend>
<div class="policy-filters tools"><label class="policy-search"><x-icon name="search"></x-icon><span class="sr-only">Search tools</span><input type="search" x-model="$store.agentEditor.toolSearch" placeholder="Search tools"></label><label>Category <select x-model="$store.agentEditor.toolCategory"><option value="all">All</option><option value="local">Local</option><option value="plugin">Plugin</option><option value="mcp">MCP</option></select></label><label>Origin <select x-model="$store.agentEditor.toolOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.toolOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
<div class="policy-lists">
<section class="policy-list" aria-labelledby="allowed-tools-title"><header><div><h4 id="allowed-tools-title">Allowed</h4><span x-text="`${$store.agentEditor.filteredTools(true).length} tools`"></span></div><button type="button" class="text-button" @click="$store.agentEditor.moveAllVisibleTools(false)">Block all shown</button></header><div class="policy-items"><template x-for="tool in $store.agentEditor.filteredTools(true)" :key="tool.id"><label><input type="checkbox" :value="tool.id" x-model="$store.agentEditor.selectedAllowedTools"><span><strong x-text="tool.label"></strong><small x-text="tool.id"></small><small x-text="tool.description"></small><em x-show="!tool.available">Unavailable · retained</em></span></label></template></div><button type="button" class="button policy-move" :disabled="!$store.agentEditor.selectedAllowedTools.length" @click="$store.agentEditor.moveTools($store.agentEditor.selectedAllowedTools, false)"><x-icon name="arrow_forward"></x-icon>Block selected</button></section>
<section class="policy-list" aria-labelledby="blocked-tools-title"><header><div><h4 id="blocked-tools-title">Blocked</h4><span x-text="`${$store.agentEditor.filteredTools(false).length} tools`"></span></div><button type="button" class="text-button" @click="$store.agentEditor.moveAllVisibleTools(true)">Allow all shown</button></header><div class="policy-items"><template x-for="tool in $store.agentEditor.filteredTools(false)" :key="tool.id"><label><input type="checkbox" :value="tool.id" x-model="$store.agentEditor.selectedBlockedTools"><span><strong x-text="tool.label"></strong><small x-text="tool.id"></small><small x-text="tool.description"></small><em x-show="!tool.available">Unavailable · retained</em></span></label></template></div><button type="button" class="button policy-move" :disabled="!$store.agentEditor.selectedBlockedTools.length" @click="$store.agentEditor.moveTools($store.agentEditor.selectedBlockedTools, true)"><x-icon name="arrow_back"></x-icon>Allow selected</button></section>
<section class="policy-list" aria-labelledby="allowed-tools-title"><header><div><h4 id="allowed-tools-title">Allowed</h4><span x-text="`${$store.agentEditor.filteredTools(true).length} tools`"></span></div><button type="button" class="button policy-bulk" x-show="$store.agentEditor.draft.toolPolicy.mode === 'custom' && $store.agentEditor.filteredTools(true).length" @click="$store.agentEditor.moveAllVisibleTools(false)" x-text="`Block ${$store.agentEditor.filteredTools(true).length} shown`"></button></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredTools(true).length">No allowed tools — select items on the right, or allow new tools automatically.</p><template x-for="tool in $store.agentEditor.filteredTools(true)" :key="tool.id"><div class="policy-item"><input type="checkbox" :value="tool.id" x-model="$store.agentEditor.selectedAllowedTools" :aria-label="`Select ${tool.label}`"><div><strong x-text="tool.label"></strong><small x-text="tool.id"></small><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p><em x-show="!tool.available">Unavailable — kept in your settings</em></div></div></template></div></section>
<div class="policy-transfer-actions" role="group" aria-label="Move selected tools"><button type="button" class="button icon" :disabled="!$store.agentEditor.selectedAllowedTools.length" aria-label="Block selected tools" @click="$store.agentEditor.moveTools($store.agentEditor.selectedAllowedTools, false)"><x-icon name="arrow_forward"></x-icon></button><button type="button" class="button icon" :disabled="!$store.agentEditor.selectedBlockedTools.length" aria-label="Allow selected tools" @click="$store.agentEditor.moveTools($store.agentEditor.selectedBlockedTools, true)"><x-icon name="arrow_back"></x-icon></button></div>
<section class="policy-list" aria-labelledby="blocked-tools-title"><header><div><h4 id="blocked-tools-title">Blocked</h4><span x-text="`${$store.agentEditor.filteredTools(false).length} tools`"></span></div><button type="button" class="button policy-bulk" x-show="$store.agentEditor.draft.toolPolicy.mode === 'custom' && $store.agentEditor.filteredTools(false).length" @click="$store.agentEditor.moveAllVisibleTools(true)" x-text="`Allow ${$store.agentEditor.filteredTools(false).length} shown`"></button></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredTools(false).length" x-text="$store.agentEditor.draft.toolPolicy.mode === 'custom' ? 'No blocked tools — select items on the left, or block new tools until reviewed.' : 'Choose tools to customize standard access.'"></p><template x-for="tool in $store.agentEditor.filteredTools(false)" :key="tool.id"><div class="policy-item"><input type="checkbox" :value="tool.id" x-model="$store.agentEditor.selectedBlockedTools" :aria-label="`Select ${tool.label}`"><div><strong x-text="tool.label"></strong><small x-text="tool.id"></small><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p><em x-show="!tool.available">Unavailable — kept in your settings</em></div></div></template></div></section>
</div>
</div></template>
<fieldset class="future-default"><legend>When new tools are installed later</legend><label><input type="radio" name="tool-future-default" value="allow" :checked="$store.agentEditor.draft.toolPolicy.default === 'allow'" @change="$store.agentEditor.setPolicyDefault('tool','allow')">Allow automatically</label><label><input type="radio" name="tool-future-default" value="block" :checked="$store.agentEditor.draft.toolPolicy.default === 'block'" @change="$store.agentEditor.setPolicyDefault('tool','block')">Block until reviewed</label></fieldset>
</fieldset>
</section>
<section x-show="$store.agentEditor.section === '4'" data-agent-editor-section="4" tabindex="-1" aria-labelledby="agent-section-4-title">
<div class="advanced-section-heading"><div><span>Section 4</span><h3 id="agent-section-4-title">Skills</h3></div><p>Allowed means discoverable and loadable; it does not pin or activate a skill.</p></div>
<div class="policy-mode-row"><label><input type="radio" name="skill-policy-mode" :checked="$store.agentEditor.draft.skillPolicy.mode === 'inherit'" @change="$store.agentEditor.draft.skillPolicy = {mode:'inherit',default:'allow',allowed:[],blocked:[]}">Use standard skill access</label><label><input type="radio" name="skill-policy-mode" :checked="$store.agentEditor.draft.skillPolicy.mode === 'custom'" @change="$store.agentEditor.draft.skillPolicy.mode = 'custom'">Choose skills</label></div>
<template x-if="$store.agentEditor.draft.skillPolicy.mode === 'custom'"><div>
<div class="future-default"><strong>New skills are:</strong><label><input type="radio" name="skill-future-default" :checked="$store.agentEditor.draft.skillPolicy.default === 'allow'" @change="$store.agentEditor.setPolicyDefault('skill','allow')">Allowed</label><label><input type="radio" name="skill-future-default" :checked="$store.agentEditor.draft.skillPolicy.default === 'block'" @change="$store.agentEditor.setPolicyDefault('skill','block')">Blocked</label></div>
<div class="policy-filters"><label><span class="sr-only">Search skills</span><input type="search" x-model="$store.agentEditor.skillSearch" placeholder="Search skills"></label><label>Origin <select x-model="$store.agentEditor.skillOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.skillOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
<header class="advanced-section-heading"><h3 id="agent-section-4-title">Skills</h3><p>Choose which skills this agent can find and use.</p></header>
<div class="policy-mode-row"><label><input type="radio" name="skill-policy-mode" :checked="$store.agentEditor.draft.skillPolicy.mode === 'inherit'" @change="$store.agentEditor.useStandardSkills()"><span>Use standard skill access <small x-text="`(${$store.agentEditor.skillCatalog.length} skills)`"></small></span></label><label><input type="radio" name="skill-policy-mode" :checked="$store.agentEditor.draft.skillPolicy.mode === 'custom'" @change="$store.agentEditor.chooseSkills()">Choose skills</label></div>
<fieldset class="policy-editor" :disabled="$store.agentEditor.draft.skillPolicy.mode !== 'custom'">
<legend class="sr-only">Skill access selection</legend>
<div class="policy-filters skills"><label class="policy-search"><x-icon name="search"></x-icon><span class="sr-only">Search skills</span><input type="search" x-model="$store.agentEditor.skillSearch" placeholder="Search skills"></label><label>Origin <select x-model="$store.agentEditor.skillOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.skillOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
<div class="policy-lists">
<section class="policy-list" aria-labelledby="allowed-skills-title"><header><div><h4 id="allowed-skills-title">Allowed</h4><span x-text="`${$store.agentEditor.filteredSkills(true).length} skills`"></span></div><button type="button" class="text-button" @click="$store.agentEditor.moveAllVisibleSkills(false)">Block all shown</button></header><div class="policy-items"><template x-for="skill in $store.agentEditor.filteredSkills(true)" :key="skill.path"><label><input type="checkbox" :value="skill.name" x-model="$store.agentEditor.selectedAllowedSkills"><span><strong x-text="skill.name"></strong><small x-text="skill.description"></small><small x-text="skill.origin"></small><em x-show="skill.available === false">Unavailable · retained</em><em x-show="$store.agentEditor.skillWarnings(skill).length" x-text="`Expects blocked tool: ${$store.agentEditor.skillWarnings(skill).join(', ')}`"></em></span></label></template></div><button type="button" class="button policy-move" :disabled="!$store.agentEditor.selectedAllowedSkills.length" @click="$store.agentEditor.moveSkills($store.agentEditor.selectedAllowedSkills, false)"><x-icon name="arrow_forward"></x-icon>Block selected</button></section>
<section class="policy-list" aria-labelledby="blocked-skills-title"><header><div><h4 id="blocked-skills-title">Blocked</h4><span x-text="`${$store.agentEditor.filteredSkills(false).length} skills`"></span></div><button type="button" class="text-button" @click="$store.agentEditor.moveAllVisibleSkills(true)">Allow all shown</button></header><div class="policy-items"><template x-for="skill in $store.agentEditor.filteredSkills(false)" :key="skill.path"><label><input type="checkbox" :value="skill.name" x-model="$store.agentEditor.selectedBlockedSkills"><span><strong x-text="skill.name"></strong><small x-text="skill.description"></small><small x-text="skill.origin"></small><em x-show="skill.available === false">Unavailable · retained</em></span></label></template></div><button type="button" class="button policy-move" :disabled="!$store.agentEditor.selectedBlockedSkills.length" @click="$store.agentEditor.moveSkills($store.agentEditor.selectedBlockedSkills, true)"><x-icon name="arrow_back"></x-icon>Allow selected</button></section>
<section class="policy-list" aria-labelledby="allowed-skills-title"><header><div><h4 id="allowed-skills-title">Allowed</h4><span x-text="`${$store.agentEditor.filteredSkills(true).length} skills`"></span></div><button type="button" class="button policy-bulk" x-show="$store.agentEditor.draft.skillPolicy.mode === 'custom' && $store.agentEditor.filteredSkills(true).length" @click="$store.agentEditor.moveAllVisibleSkills(false)" x-text="`Block ${$store.agentEditor.filteredSkills(true).length} shown`"></button></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredSkills(true).length">No allowed skills — select items on the right, or allow new skills automatically.</p><template x-for="skill in $store.agentEditor.filteredSkills(true)" :key="skill.path"><div class="policy-item"><input type="checkbox" :value="skill.name" x-model="$store.agentEditor.selectedAllowedSkills" :aria-label="`Select ${skill.name}`"><div><strong x-text="skill.name"></strong><small x-text="skill.origin"></small><p class="policy-item-description" x-show="skill.description" x-text="skill.description"></p><em x-show="skill.available === false">Unavailable — kept in your settings</em><em x-show="$store.agentEditor.skillWarnings(skill).length" x-text="`Expects blocked tool: ${$store.agentEditor.skillWarnings(skill).join(', ')}`"></em></div></div></template></div></section>
<div class="policy-transfer-actions" role="group" aria-label="Move selected skills"><button type="button" class="button icon" :disabled="!$store.agentEditor.selectedAllowedSkills.length" aria-label="Block selected skills" @click="$store.agentEditor.moveSkills($store.agentEditor.selectedAllowedSkills, false)"><x-icon name="arrow_forward"></x-icon></button><button type="button" class="button icon" :disabled="!$store.agentEditor.selectedBlockedSkills.length" aria-label="Allow selected skills" @click="$store.agentEditor.moveSkills($store.agentEditor.selectedBlockedSkills, true)"><x-icon name="arrow_back"></x-icon></button></div>
<section class="policy-list" aria-labelledby="blocked-skills-title"><header><div><h4 id="blocked-skills-title">Blocked</h4><span x-text="`${$store.agentEditor.filteredSkills(false).length} skills`"></span></div><button type="button" class="button policy-bulk" x-show="$store.agentEditor.draft.skillPolicy.mode === 'custom' && $store.agentEditor.filteredSkills(false).length" @click="$store.agentEditor.moveAllVisibleSkills(true)" x-text="`Allow ${$store.agentEditor.filteredSkills(false).length} shown`"></button></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredSkills(false).length" x-text="$store.agentEditor.draft.skillPolicy.mode === 'custom' ? 'No blocked skills — select items on the left, or block new skills until reviewed.' : 'Choose skills to customize standard access.'"></p><template x-for="skill in $store.agentEditor.filteredSkills(false)" :key="skill.path"><div class="policy-item"><input type="checkbox" :value="skill.name" x-model="$store.agentEditor.selectedBlockedSkills" :aria-label="`Select ${skill.name}`"><div><strong x-text="skill.name"></strong><small x-text="skill.origin"></small><p class="policy-item-description" x-show="skill.description" x-text="skill.description"></p><em x-show="skill.available === false">Unavailable — kept in your settings</em></div></div></template></div></section>
</div>
</div></template>
<fieldset class="future-default"><legend>When new skills are installed later</legend><label><input type="radio" name="skill-future-default" :checked="$store.agentEditor.draft.skillPolicy.default === 'allow'" @change="$store.agentEditor.setPolicyDefault('skill','allow')">Allow automatically</label><label><input type="radio" name="skill-future-default" :checked="$store.agentEditor.draft.skillPolicy.default === 'block'" @change="$store.agentEditor.setPolicyDefault('skill','block')">Block until reviewed</label></fieldset>
</fieldset>
</section>
<section x-show="$store.agentEditor.section === '5'" data-agent-editor-section="5" tabindex="-1" aria-labelledby="agent-section-5-title">
<div class="advanced-section-heading"><div><span>Section 5</span><h3 id="agent-section-5-title">Review & test</h3></div><p>The save changes exactly these user-layer files and no others.</p></div>
<button type="button" class="button" @click="$store.agentEditor.previewPlan()" :disabled="$store.agentEditor.planLoading"><x-icon :class="{ spinning: $store.agentEditor.planLoading }" name="refresh"></x-icon>Refresh change plan</button>
<div class="change-plan" aria-live="polite">
<header class="advanced-section-heading"><h3 id="agent-section-5-title">Review</h3><p>Saving will change exactly these files — nothing else.</p></header>
<div class="review-plan-status" role="status" x-show="$store.agentEditor.planStatus === 'loading'"><x-icon class="spinning" name="progress_activity"></x-icon><span>Computing the exact change plan…</span></div>
<div class="review-plan-status" x-show="$store.agentEditor.planStatus === 'error'"><span>The change plan could not be computed.</span><button type="button" class="text-button" @click="$store.agentEditor.previewPlan()">Retry</button></div>
<div class="review-blocked" role="alert" x-show="$store.agentEditor.planStatus === 'blocked'">
<x-icon name="error"></x-icon>
<div><strong x-text="`Fix ${$store.agentEditor.validationIssues().length} ${$store.agentEditor.validationIssues().length === 1 ? 'issue' : 'issues'} to see the change plan`"></strong>
<ul><template x-for="issue in $store.agentEditor.validationIssues()" :key="`${issue.section}-${issue.field}-${issue.message}`"><li><button type="button" class="text-button" @click="$store.agentEditor.showValidationIssue(issue)" x-text="`${issue.label}: ${issue.message} — fix`"></button></li></template></ul>
</div>
</div>
<div class="change-plan" aria-live="polite" x-show="$store.agentEditor.planStatus === 'ready'">
<section><h4>Will create or update</h4><template x-if="!$store.agentEditor.plan.written?.length"><p>None</p></template><ul><template x-for="path in $store.agentEditor.plan.written || []" :key="path"><li><code x-text="path"></code></li></template></ul></section>
<section><h4>Will delete</h4><template x-if="!$store.agentEditor.plan.deleted?.length"><p>None</p></template><ul><template x-for="path in $store.agentEditor.plan.deleted || []" :key="path"><li><code x-text="path"></code></li></template></ul></section>
<template x-if="$store.agentEditor.plan.warnings?.length"><section><h4>Notices</h4><ul><template x-for="warning in $store.agentEditor.plan.warnings" :key="warning"><li x-text="warning"></li></template></ul></section></template>
</div>
<div class="review-actions" x-show="$store.agentEditor.pendingMutation"><button type="button" class="button danger" @click="$store.agentEditor.applyPendingMutation()">Apply removal plan</button><button type="button" class="text-button" @click="$store.agentEditor.pendingMutation = null; $store.agentEditor.previewPlan()">Cancel removal</button></div>
<div class="review-actions" x-show="$store.agentEditor.pendingMutation"><button type="button" class="button danger" @click="$store.agentEditor.applyPendingMutation()"><x-icon name="delete"></x-icon>Apply removal plan</button><button type="button" class="text-button" @click="$store.agentEditor.pendingMutation = null; $store.agentEditor.previewPlan()">Cancel removal</button></div>
<div class="profile-maintenance" x-show="!$store.agentEditor.draft.creating && $store.agentEditor.state.profile.origin !== 'Custom'">
<h4>Built-in or plugin profile</h4><p>Remove only editor-managed keys and known inherited prompt overrides. Manual and unknown files stay in place.</p><button type="button" class="button" @click="$store.agentEditor.planRemoval(false)">Remove my changes</button>
<details><summary>Destructive user-layer cleanup</summary><p>Deletes the entire user override directory after showing every file. Bundled files remain untouched.</p><button type="button" class="button danger" @click="$store.agentEditor.planRemoval(true)">Plan full user-override deletion</button></details>
<h4>Built-in or plugin profile</h4><p>Remove only the customizations shown in this editor. Other files stay in place.</p><button type="button" class="button danger" @click="$store.agentEditor.planRemoval(false)"><x-icon name="delete_sweep"></x-icon>Remove my changes</button>
<details><summary>Delete all customizations for this profile</summary><p>This removes every customization saved for this profile after showing each affected file. Agent Zeros defaults remain untouched.</p><button type="button" class="button danger" @click="$store.agentEditor.planRemoval(true)"><x-icon name="delete_forever"></x-icon>Review files to delete</button></details>
</div>
<div class="profile-maintenance" x-show="!$store.agentEditor.draft.creating && $store.agentEditor.state.profile.origin === 'Custom'"><h4>Delete custom agent</h4><p>Project references, active sessions, scoped plugins, skills, tools, and assets are shown before confirmation.</p><button type="button" class="button danger" @click="$store.agentEditor.deleteProfile($store.agentEditor.draft.profileId)">Delete agent</button></div>
<div class="profile-maintenance" x-show="!$store.agentEditor.draft.creating && $store.agentEditor.state.profile.origin === 'Custom'"><h4>Delete custom agent</h4><p>Projects and open chats that use this agent are shown before confirmation, together with its files and settings.</p><button type="button" class="button danger" @click="$store.agentEditor.deleteProfile($store.agentEditor.draft.profileId)">Delete agent</button></div>
</section>
</div>
</div>
@ -279,74 +281,89 @@
<div class="modal-footer agent-editor-footer" data-modal-footer x-show="!$store.agentEditor.loading">
<div class="footer-left">
<button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'manage'" @click="window.closeModal?.()">Close</button>
<button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'editor' && $store.agentEditor.mode === 'easy'" @click="window.closeModal?.()">Cancel</button>
<button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'editor' && $store.agentEditor.mode === 'advanced'" @click="$store.agentEditor.setMode('easy')">Back to Easy</button>
<button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'editor'" @click="window.closeModal?.()">Cancel</button>
</div>
<div class="footer-actions">
<button type="button" class="btn btn-ok" x-show="$store.agentEditor.view === 'manage'" @click="$store.agentEditor.loadEditor('new-agent', true)">Create agent</button>
<button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'editor' && !$store.agentEditor.draft?.creating" @click="$store.agentEditor.save(true)" :disabled="$store.agentEditor.saving">Save & test</button>
<button type="button" class="btn btn-ok" x-show="$store.agentEditor.view === 'editor'" @click="$store.agentEditor.save(false)" :disabled="$store.agentEditor.saving || $store.agentEditor.avatarUploading"><span x-text="$store.agentEditor.saving ? 'Saving…' : $store.agentEditor.draft?.creating ? 'Create agent' : 'Save changes'"></span></button>
<button type="button" class="btn agent-editor-secondary-action" x-show="$store.agentEditor.view === 'editor' && !$store.agentEditor.draft?.creating" @click="$store.agentEditor.save(true)" :disabled="$store.agentEditor.saving || $store.agentEditor.validationIssues().length">Save & test</button>
<button type="button" class="btn btn-ok" x-show="$store.agentEditor.view === 'editor'" @click="$store.agentEditor.save(false)" :disabled="$store.agentEditor.saving || $store.agentEditor.avatarUploading || $store.agentEditor.validationIssues().length" :title="$store.agentEditor.validationIssues().length ? 'Fix the highlighted issues before saving' : ''"><span x-text="$store.agentEditor.saving ? 'Saving…' : $store.agentEditor.draft?.creating ? 'Create agent' : 'Save changes'"></span></button>
</div>
</div>
</div>
<style>
.agent-editor { color: var(--color-text); min-height: 12rem; }
.agent-editor {
--color-text-secondary: var(--color-text-muted);
--color-error: var(--color-error-text);
--color-warning: var(--color-warning-text);
--agent-editor-action: var(--color-highlight);
--agent-editor-change: var(--color-warning-text);
--agent-editor-danger: var(--color-error-text);
color: var(--color-text);
min-height: 12rem;
min-width: 0;
}
.agent-editor h2,.agent-editor h3,.agent-editor h4,.agent-editor p { margin: 0; }
.agent-editor button,.agent-editor input,.agent-editor textarea,.agent-editor select { font: inherit; }
.agent-editor-error { display:flex; gap:.55rem; align-items:flex-start; margin:0 0 .8rem; padding:.7rem .8rem; border:1px solid color-mix(in srgb,var(--color-error) 55%,var(--color-border)); border-radius:8px; background:color-mix(in srgb,var(--color-error) 10%,var(--color-panel)); }
.agent-editor input[type="search"],.agent-editor select { min-width:0; min-height:2.25rem; box-sizing:border-box; padding:.5rem .6rem; border:1px solid var(--color-border); border-radius:7px; background:var(--color-input); color:var(--color-text); color-scheme:dark; }
.agent-editor input[type="checkbox"],.agent-editor input[type="radio"] { appearance:none; flex:0 0 auto; width:1.5rem; height:1.5rem; margin:.05rem 0; border:1px solid var(--color-border); background:var(--color-input); }
.agent-editor input[type="checkbox"] { display:grid; place-content:center; border-radius:4px; }
.agent-editor input[type="checkbox"]::before { content:""; width:.55rem; height:.3rem; border:solid white; border-width:0 0 2px 2px; opacity:0; transform:translateY(-1px) rotate(-45deg); }
.agent-editor input[type="checkbox"]:checked { border-color:var(--agent-editor-action); background:var(--agent-editor-action); }
.agent-editor input[type="checkbox"]:checked::before { opacity:1; }
.agent-editor input[type="radio"] { border-radius:50%; }
.agent-editor input[type="radio"]:checked { border-color:var(--agent-editor-action); box-shadow:inset 0 0 0 4px var(--color-input); background:var(--agent-editor-action); }
.agent-editor-error { display:flex; gap:.55rem; align-items:flex-start; margin:0 0 .8rem; padding:.7rem .8rem; border:1px solid color-mix(in srgb,var(--agent-editor-danger) 55%,var(--color-border)); border-radius:8px; background:color-mix(in srgb,var(--agent-editor-danger) 10%,var(--color-panel)); }
.agent-editor-error span { flex:1; white-space:pre-wrap; }
.agent-editor-loading { min-height:20rem; display:grid; place-content:center; justify-items:center; gap:.65rem; color:var(--color-text-secondary); }
.agent-editor-topbar { display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-bottom:1rem; }
.agent-editor-heading { display:flex; align-items:center; gap:.6rem; }
.agent-editor-heading h2,.agent-manager-heading h2 { font-size:1.2rem; }
.agent-editor-subtitle { margin-top:.2rem; font-size:.78rem; color:var(--color-text-secondary); }
.dirty-dot,.section-dirty,.agent-change-dot { color:var(--color-accent); }
.agent-mode-switch { display:flex; padding:3px; border:1px solid var(--color-border); border-radius:9px; background:var(--color-input); }
.agent-status-badge { display:inline-flex; align-items:center; gap:.25rem; max-width:100%; padding:.2rem .45rem; border:1px solid var(--color-border); border-radius:999px; font-size:.7rem; font-weight:500; line-height:1.2; white-space:normal; }
.agent-status-badge x-icon { flex:0 0 auto; font-size:.85rem; }
.agent-status-badge.is-customized,.agent-status-badge.is-unsaved { color:var(--agent-editor-change); border-color:color-mix(in srgb,var(--agent-editor-change) 44%,var(--color-border)); background:color-mix(in srgb,var(--agent-editor-change) 8%,transparent); }
.agent-status-badge.is-error { color:var(--agent-editor-danger); border-color:color-mix(in srgb,var(--agent-editor-danger) 48%,var(--color-border)); background:color-mix(in srgb,var(--agent-editor-danger) 8%,transparent); }
.agent-status-badge.compact { justify-self:end; padding:.15rem .35rem; font-size:.64rem; }
.agent-mode-switch { display:flex; margin-left:auto; padding:3px; border:1px solid var(--color-border); border-radius:9px; background:var(--color-input); }
.agent-mode-switch button { border:0; border-radius:6px; padding:.42rem .8rem; color:var(--color-text-secondary); background:transparent; }
.agent-mode-switch button.active { color:var(--color-text); background:var(--color-panel); box-shadow:0 1px 4px rgba(0,0,0,.2); }
.agent-easy { max-width:43rem; margin:0 auto; display:flex; flex-direction:column; gap:1.35rem; padding:.25rem .25rem 1rem; }
.agent-easy-identity { position:relative; display:grid; grid-template-columns:7.2rem 1fr; gap:1rem; align-items:center; }
.identity-link { position:absolute; top:0; right:0; }
.agent-avatar-wrap { display:flex; flex-direction:column; align-items:center; gap:.45rem; }
.agent-avatar { width:5rem; aspect-ratio:1; position:relative; display:grid; place-items:center; border-radius:16px; overflow:hidden; color:white; font-size:1.35rem; font-weight:700; box-shadow:inset 0 0 0 1px rgba(255,255,255,.15); }
.agent-avatar img { width:100%; height:100%; object-fit:cover; }
.avatar-progress { position:absolute; inset:0; display:grid; place-items:center; background:rgba(0,0,0,.5); }
.agent-avatar-actions { display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:.3rem .55rem; font-size:.76rem; }
.avatar-color-action,.avatar-upload-action { position:relative; cursor:pointer; color:var(--color-accent); }
.agent-avatar-actions { display:flex; align-items:center; justify-content:center; gap:.35rem; font-size:.76rem; white-space:nowrap; }
.avatar-color-action,.avatar-upload-action { position:relative; cursor:pointer; }
.avatar-action-icon { display:grid; place-items:center; width:1.75rem; height:1.75rem; border-radius:6px; color:var(--agent-editor-action); }
.avatar-action-icon x-icon { font-size:1.05rem; }
.avatar-color-action input,.avatar-upload-action input { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }
.agent-field { display:flex; flex-direction:column; gap:.35rem; min-width:0; }
.agent-name-field { align-self:start; }
.agent-field-label { font-weight:650; font-size:.92rem; }
.agent-field input,.agent-field textarea,.agent-easy textarea,.prompt-find input,.policy-filters input,.policy-filters select,.compact-search input { width:100%; box-sizing:border-box; }
.agent-field small,.agent-field-heading p,.agent-id-feedback,.field-status { color:var(--color-text-secondary); font-size:.79rem; }
.field-status { display:block; margin-top:.15rem; }
.field-error { color:var(--color-error); }
.field-error { display:block; color:var(--color-text-secondary); font-size:.79rem; }
.agent-id-feedback { grid-column:2; margin-top:-.6rem; }
.agent-field-heading { display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; margin-bottom:.5rem; }
.agent-advanced-link,.text-button { border:0; padding:0; background:transparent; color:var(--color-accent); cursor:pointer; font-size:.82rem; text-align:left; }
.agent-advanced-link:hover,.text-button:hover { text-decoration:underline; }
.agent-editor .text-button { display:inline-flex; align-items:center; min-height:1.5rem; border:0; padding:0; background:transparent; color:var(--agent-editor-action); cursor:pointer; font-size:.82rem; text-align:left; }
.agent-editor .text-button:hover { text-decoration:underline; }
.agent-easy textarea { min-height:11rem; resize:vertical; }
.restore-action { display:inline-flex; align-items:center; gap:.25rem; margin-top:.4rem; }
.easy-tool-summary { display:flex; align-items:center; gap:.7rem; padding:.8rem; border:1px solid var(--color-border); border-radius:10px; background:var(--color-input); }
.tool-state-icon { width:1.15rem; text-align:center; color:var(--color-accent); }
.easy-tool-copy { flex:1; display:flex; flex-direction:column; gap:.15rem; }
.easy-tool-copy span,.custom-tool-reset span { color:var(--color-text-secondary); font-size:.8rem; }
.easy-tool-choices { display:flex; flex-direction:column; gap:.5rem; padding:.7rem .8rem; border:1px solid var(--color-border); border-top:0; border-radius:0 0 10px 10px; }
.easy-tool-choices label { display:flex; gap:.5rem; align-items:flex-start; }
.easy-tool-choices label span { display:flex; flex-direction:column; }
.easy-tool-choices small { color:var(--color-text-secondary); }
.custom-tool-reset { display:flex; justify-content:space-between; gap:1rem; margin-top:.45rem; padding:0 .2rem; }
.easy-tool-list { display:flex; flex-direction:column; max-height:18rem; overflow-y:auto; padding:.2rem .35rem .2rem 0; }
.easy-tool-list .policy-item { cursor:pointer; }
.easy-skills-hint { margin:.55rem 0 0; color:var(--color-text-secondary); font-size:.79rem; }
.agent-advanced { display:grid; grid-template-columns:14rem minmax(0,1fr); gap:1rem; min-height:0; }
.agent-advanced-nav { display:flex; flex-direction:column; gap:.3rem; padding:.45rem; border:1px solid var(--color-border); border-radius:10px; background:var(--color-input); align-self:start; position:sticky; top:0; }
.agent-advanced-nav button { display:grid; grid-template-columns:1.7rem 1fr auto; align-items:center; gap:.45rem; border:0; border-radius:7px; padding:.65rem; color:var(--color-text-secondary); background:transparent; text-align:left; }
.agent-advanced-nav button.active { color:var(--color-text); background:var(--color-panel); }
.section-number { display:grid; place-items:center; width:1.55rem; height:1.55rem; border:1px solid var(--color-border); border-radius:50%; font-size:.75rem; }
.agent-advanced-content { min-width:0; }
.agent-advanced-content,.agent-editor-workspace,.agent-manager { min-width:0; }
.agent-advanced-content > section { outline:none; display:flex; flex-direction:column; gap:1rem; }
.advanced-section-heading { display:flex; justify-content:space-between; gap:1rem; padding-bottom:.8rem; border-bottom:1px solid var(--color-border); }
.advanced-section-heading > div > span { color:var(--color-accent); font-size:.75rem; text-transform:uppercase; letter-spacing:.08em; }
.advanced-section-heading h3 { margin-top:.15rem; font-size:1.2rem; }
.advanced-section-heading p { max-width:36rem; color:var(--color-text-secondary); font-size:.84rem; text-align:right; }
.advanced-section-heading { display:flex; flex-direction:column; gap:.25rem; padding-bottom:.8rem; border-bottom:1px solid var(--color-border); }
.advanced-section-heading h3 { font-size:1.2rem; }
.advanced-section-heading p { max-width:48rem; color:var(--color-text-secondary); font-size:.84rem; }
.origin-row { display:flex; flex-wrap:wrap; align-items:center; gap:.6rem; }
.built-in-note,.field-provenance { color:var(--color-text-secondary); font-size:.76rem; }
.agent-origin { padding:.2rem .45rem; border:1px solid var(--color-border); border-radius:999px; font-size:.72rem; color:var(--color-text-secondary); }
@ -354,70 +371,98 @@
.advanced-identity-grid { display:grid; grid-template-columns:9rem minmax(0,1fr); gap:1.2rem; }
.advanced-avatar { align-self:start; }
.identity-fields { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.9rem; }
.identity-fields .agent-field:last-child { grid-column:1/-1; }
.model-preset-block { display:flex; flex-direction:column; gap:.5rem; padding:1rem; border:1px solid var(--color-border); border-radius:10px; }
.model-preset-row { display:flex; gap:.6rem; align-items:flex-start; padding:.6rem; border-radius:8px; cursor:pointer; }
.model-preset-row:hover { background:var(--color-input); }
.model-preset-row > span { display:flex; flex-direction:column; gap:.2rem; min-width:0; }
.model-preset-row small { color:var(--color-text-secondary); overflow-wrap:anywhere; }
.prompt-workspace { display:grid; grid-template-columns:16rem minmax(0,1fr); gap:.8rem; min-height:34rem; }
.prompt-browser { display:flex; flex-direction:column; min-height:0; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
.prompt-groups { display:flex; flex-direction:column; max-height:14rem; overflow:auto; padding:.35rem; border-bottom:1px solid var(--color-border); }
.prompt-groups button { border:0; border-radius:6px; padding:.42rem .5rem; background:transparent; color:var(--color-text-secondary); text-align:left; font-size:.78rem; }
.prompt-groups button.active { background:var(--color-input); color:var(--color-text); }
.identity-fields .agent-field.wide { grid-column:1/-1; }
.agent-model-preset { display:flex; flex-direction:column; align-items:flex-start; gap:.75rem; }
.agent-model-preset-picker { width:100%; display:grid; grid-template-columns:minmax(0,1fr) minmax(13rem,18rem); align-items:center; gap:1rem; margin:0; }
.agent-model-preset-picker > span { display:flex; flex-direction:column; gap:.2rem; min-width:0; }
.agent-model-preset-picker small { color:var(--color-text-secondary); font-size:.79rem; }
.agent-model-preset-picker select { width:100%; }
.agent-model-preset > .button { display:inline-flex; align-items:center; gap:.35rem; }
.prompt-workspace { display:grid; grid-template-columns:minmax(13rem,16rem) minmax(0,1fr); gap:.8rem; min-width:0; height:34rem; }
.prompt-browser { display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
.compact-search { display:flex; align-items:center; gap:.3rem; padding:.45rem; border-bottom:1px solid var(--color-border); }
.compact-search input { border:0; background:transparent; outline:none; }
.agent-editor .compact-search input,.agent-editor .policy-search input { min-height:1.8rem; padding:.2rem; border:0; background:transparent; }
.prompt-file-list { flex:1; overflow:auto; padding:.35rem; }
.prompt-file-group { min-width:0; margin-bottom:.55rem; }
.prompt-file-group h4 { padding:.35rem .5rem .25rem; color:var(--color-text-secondary); font-size:.7rem; font-weight:650; letter-spacing:.03em; text-transform:uppercase; }
.prompt-file-list button { width:100%; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:.15rem .4rem; padding:.5rem; border:0; border-radius:6px; background:transparent; color:var(--color-text); text-align:left; }
.prompt-file-list button.active { background:var(--color-input); }
.prompt-file-name { grid-column:1/-1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:monospace; font-size:.76rem; }
.prompt-file-state { color:var(--color-text-secondary); font-size:.7rem; }
.prompt-editor { display:flex; flex-direction:column; gap:.55rem; min-width:0; }
.prompt-empty { padding:.65rem; color:var(--color-text-secondary); font-size:.78rem; }
.prompt-editor { display:flex; flex-direction:column; gap:.55rem; min-width:0; max-width:100%; max-height:100%; overflow:auto; }
.prompt-editor-header { display:flex; justify-content:space-between; gap:.7rem; align-items:flex-start; }
.prompt-editor-header > div:first-child { min-width:0; overflow-wrap:anywhere; }
.source-chain { margin-top:.2rem; color:var(--color-text-secondary); font-size:.75rem; }
.prompt-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:.35rem; }
.prompt-find { display:flex; align-items:center; gap:.35rem; }
.prompt-find label { flex:1; }
.prompt-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:.35rem; min-width:0; }
.prompt-actions .button { max-width:100%; white-space:normal; overflow-wrap:anywhere; }
.prompt-view-tabs { display:flex; flex-wrap:wrap; gap:.25rem; padding-bottom:.35rem; border-bottom:1px solid var(--color-border); }
.prompt-view-tabs button { min-height:2rem; padding:.35rem .65rem; border:0; border-radius:6px; background:transparent; color:var(--color-text-secondary); }
.prompt-view-tabs button.active { background:var(--color-input); color:var(--color-text); }
.prompt-find { display:flex; flex-wrap:wrap; align-items:center; gap:.35rem; min-width:0; }
.prompt-find label { flex:1 1 10rem; min-width:0; }
.prompt-find span { color:var(--color-text-secondary); font-size:.75rem; white-space:nowrap; }
.prompt-panes { display:grid; grid-template-columns:1fr; gap:.65rem; min-height:23rem; }
.prompt-panes { display:grid; grid-template-columns:1fr; gap:.65rem; min-height:18rem; }
.prompt-panes.compare { grid-template-columns:1fr 1fr; }
.prompt-pane { min-width:0; display:flex; flex-direction:column; gap:.35rem; }
.prompt-pane label,.prompt-pane-title { color:var(--color-text-secondary); font-size:.75rem; }
.prompt-pane textarea,.prompt-pane pre,.effective-preview pre { flex:1; box-sizing:border-box; width:100%; min-height:23rem; margin:0; padding:.75rem; overflow:auto; border:1px solid var(--color-border); border-radius:8px; background:var(--color-input); color:var(--color-text); font:13px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace; white-space:pre-wrap; tab-size:2; resize:vertical; }
.prompt-pane textarea,.prompt-pane pre,.effective-preview pre { flex:1; box-sizing:border-box; width:100%; min-height:18rem; margin:0; padding:.75rem; overflow:auto; border:1px solid var(--color-border); border-radius:8px; background:var(--color-input); color:var(--color-text); font:13px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace; white-space:pre-wrap; tab-size:2; resize:vertical; }
.prompt-pane textarea:focus-visible { outline-offset:-2px; }
.prompt-pane.inherited pre { opacity:.86; }
.effective-preview { border:1px solid var(--color-border); border-radius:8px; padding:.55rem .7rem; }
.effective-preview > summary,.profile-maintenance summary { display:flex; align-items:center; min-height:1.5rem; cursor:pointer; }
.effective-preview p { margin:.5rem 0; color:var(--color-text-secondary); font-size:.78rem; }
.effective-preview pre { min-height:10rem; max-height:25rem; resize:none; }
.policy-mode-row,.future-default { display:flex; flex-wrap:wrap; align-items:center; gap:1rem; padding:.7rem; border:1px solid var(--color-border); border-radius:9px; }
.policy-mode-row,.future-default { display:flex; flex-wrap:wrap; align-items:center; gap:.7rem 1rem; padding:.7rem; border:1px solid var(--color-border); border-radius:9px; }
.policy-mode-row label,.future-default label { display:flex; gap:.35rem; align-items:center; }
.future-default { margin-bottom:.7rem; }
.policy-filters { display:grid; grid-template-columns:minmax(12rem,1fr) auto auto; gap:.6rem; margin-bottom:.7rem; }
.policy-mode-row small { color:var(--color-text-secondary); }
.policy-editor { min-width:0; margin:.75rem 0 0; padding:0; border:0; transition:opacity .15s ease; }
.policy-editor:disabled { opacity:.48; }
.future-default { margin-top:.8rem; }
.future-default legend { padding:0 .35rem; color:var(--color-text-secondary); font-size:.8rem; }
.policy-filters { display:grid; grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto) minmax(8rem,auto); gap:.6rem; margin-bottom:.7rem; }
.policy-filters.skills { grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto); }
.policy-filters label { display:flex; align-items:center; gap:.35rem; font-size:.78rem; color:var(--color-text-secondary); }
.policy-lists { display:grid; grid-template-columns:1fr 1fr; gap:.8rem; }
.policy-filters select { flex:1; }
.policy-search { min-height:2.25rem; padding:.25rem .5rem; border:1px solid var(--color-border); border-radius:7px; background:var(--color-input); color:var(--color-text-secondary); }
.policy-lists { display:grid; grid-template-columns:minmax(0,1fr) 2.5rem minmax(0,1fr); gap:.55rem; }
.policy-transfer-actions { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.5rem; }
.policy-transfer-actions .button { width:2.5rem; height:2.5rem; padding:0; justify-content:center; }
.policy-list { display:flex; flex-direction:column; min-width:0; min-height:28rem; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
.policy-list header { display:flex; justify-content:space-between; gap:.5rem; padding:.7rem; border-bottom:1px solid var(--color-border); background:var(--color-input); }
.policy-list header { display:flex; flex-wrap:wrap; justify-content:space-between; gap:.5rem; padding:.7rem; border-bottom:1px solid var(--color-border); background:var(--color-input); }
.policy-list h4 { display:inline; margin-right:.35rem; }
.policy-list header span { color:var(--color-text-secondary); font-size:.72rem; }
.policy-items { flex:1; max-height:32rem; overflow:auto; padding:.35rem; }
.policy-items label { display:flex; gap:.55rem; align-items:flex-start; padding:.55rem; border-radius:7px; }
.policy-items label:hover { background:var(--color-input); }
.policy-items label > span { display:flex; flex-direction:column; gap:.15rem; min-width:0; }
.policy-item { display:flex; gap:.55rem; align-items:flex-start; padding:.55rem; border-radius:7px; }
.policy-item:hover { background:var(--color-input); }
.policy-item > div { display:flex; flex:1; flex-direction:column; gap:.15rem; min-width:0; }
.policy-items small { color:var(--color-text-secondary); overflow-wrap:anywhere; }
.policy-items em { color:var(--color-warning); font-size:.72rem; font-style:normal; }
.policy-move { margin:.55rem; justify-content:center; }
.policy-item-description { margin:0; color:var(--color-text-secondary); font-size:.75rem; white-space:pre-wrap; overflow-wrap:anywhere; }
.policy-empty { padding:1rem .7rem; color:var(--color-text-secondary); font-size:.8rem; text-align:center; }
.policy-bulk { min-height:2rem; padding:.25rem .55rem; white-space:normal; }
.change-plan { display:grid; grid-template-columns:1fr 1fr; gap:.8rem; }
.change-plan section { padding:.85rem; border:1px solid var(--color-border); border-radius:9px; background:var(--color-input); min-width:0; }
.change-plan ul { margin:.55rem 0 0; padding-left:1.2rem; }
.change-plan code { overflow-wrap:anywhere; }
.review-blocked { display:flex; align-items:flex-start; gap:.65rem; padding:.8rem; border:1px solid color-mix(in srgb,var(--agent-editor-danger) 55%,var(--color-border)); border-radius:9px; background:color-mix(in srgb,var(--agent-editor-danger) 8%,var(--color-panel)); }
.review-blocked > x-icon { flex:0 0 auto; color:var(--agent-editor-danger); }
.review-blocked ul { margin:.45rem 0 0; padding-left:1.1rem; }
.review-plan-status { display:flex; flex-wrap:wrap; align-items:center; gap:.45rem; color:var(--color-text-secondary); font-size:.82rem; }
.review-actions { display:flex; flex-wrap:wrap; align-items:center; gap:.65rem; }
.profile-maintenance { display:flex; flex-direction:column; align-items:flex-start; gap:.55rem; margin-top:.5rem; padding:1rem; border:1px solid var(--color-border); border-radius:9px; }
.profile-maintenance p { color:var(--color-text-secondary); font-size:.82rem; }
.profile-maintenance details { width:100%; padding-top:.6rem; border-top:1px solid var(--color-border); }
.profile-maintenance details p { margin:.5rem 0; }
.agent-editor-footer { width:100%; display:flex; justify-content:space-between; gap:.7rem; }
.agent-editor-footer { --agent-editor-action:var(--color-highlight); width:100%; display:flex; justify-content:space-between; gap:.7rem; }
.footer-actions { display:flex; gap:.5rem; margin-left:auto; }
.agent-editor-footer .btn-cancel,.agent-editor-secondary-action { border:1px solid var(--color-border); background:transparent; color:var(--color-text); }
.agent-editor-footer .btn-cancel:hover,.agent-editor-secondary-action:hover { border-color:color-mix(in srgb,var(--agent-editor-action) 50%,var(--color-border)); background:color-mix(in srgb,var(--agent-editor-action) 10%,transparent); color:var(--color-text); }
.agent-editor-secondary-action { display:inline-flex; align-items:center; color:color-mix(in srgb,#fff 82%,var(--agent-editor-action)); }
.agent-editor .button.danger { display:inline-flex; align-items:center; gap:.35rem; border-color:color-mix(in srgb,var(--agent-editor-danger) 60%,var(--color-border)); color:var(--agent-editor-danger); background:color-mix(in srgb,var(--agent-editor-danger) 7%,var(--color-panel)); }
.agent-editor .button.danger:hover { background:color-mix(in srgb,var(--agent-editor-danger) 15%,var(--color-panel)); }
.agent-manager { display:flex; flex-direction:column; gap:1rem; }
.agent-manager-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; }
.agent-manager-heading p,.agent-manager-copy p { color:var(--color-text-secondary); font-size:.82rem; }
.agent-manager-intro,.agent-manager-copy p { color:var(--color-text-secondary); font-size:.82rem; }
.agent-manager-list { display:flex; flex-direction:column; gap:.55rem; }
.agent-manager-card { display:grid; grid-template-columns:3rem minmax(0,1fr) auto; gap:.75rem; align-items:center; padding:.75rem; border:1px solid var(--color-border); border-radius:10px; }
.agent-manager-avatar { width:3rem; aspect-ratio:1; display:grid; place-items:center; border-radius:10px; color:white; font-weight:700; overflow:hidden; }
@ -425,29 +470,31 @@
.agent-manager-copy { min-width:0; }
.agent-manager-name { display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; }
.agent-manager-copy code { font-size:.7rem; color:var(--color-text-secondary); }
.agent-manager-actions { display:flex; gap:.4rem; }
.agent-manager-actions { display:flex; flex-wrap:wrap; gap:.4rem; }
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
.toast-link { border:0; background:transparent; color:var(--color-accent); text-decoration:underline; cursor:pointer; }
.agent-editor :focus-visible { outline:2px solid var(--color-accent); outline-offset:2px; }
.toast-link { border:0; background:transparent; color:var(--agent-editor-action); text-decoration:underline; cursor:pointer; }
.agent-editor :focus-visible { outline:2px solid var(--agent-editor-action); outline-offset:2px; }
.modal-inner.agent-editor-easy { width:min(720px,calc(100vw - 2rem)); max-width:720px; }
.modal-inner.agent-editor-advanced { width:calc(100vw - 2rem); max-width:none; height:calc(100vh - 2rem); }
.modal-inner.agent-editor-advanced .modal-bd { min-height:0; }
.modal-inner.agent-editor-advanced .agent-editor,.modal-inner.agent-editor-advanced .agent-editor-workspace { min-height:100%; }
@media (max-width: 760px) {
.modal-inner.agent-editor-easy,.modal-inner.agent-editor-advanced { width:100vw; max-width:none; height:100vh; max-height:none; border-radius:0; }
.agent-editor-topbar,.advanced-section-heading { align-items:flex-start; }
.advanced-section-heading { flex-direction:column; }
.advanced-section-heading p { text-align:left; }
.agent-editor-topbar { align-items:flex-start; }
.agent-easy-identity { grid-template-columns:1fr; justify-items:center; padding-top:1.8rem; }
.agent-name-field { width:100%; }
.agent-id-feedback { grid-column:1; width:100%; margin:0; }
.agent-advanced { grid-template-columns:1fr; }
.agent-advanced-nav { position:static; flex-direction:row; overflow-x:auto; }
.agent-advanced-nav button { grid-template-columns:auto auto auto; white-space:nowrap; }
.prompt-workspace { grid-template-columns:1fr; }
.prompt-browser { max-height:22rem; }
.agent-model-preset-picker { grid-template-columns:1fr; gap:.5rem; }
.prompt-workspace { grid-template-columns:1fr; height:auto; }
.prompt-browser { height:22rem; max-height:22rem; }
.prompt-editor { max-height:none; overflow:visible; }
.prompt-panes.compare,.policy-lists,.change-plan,.advanced-identity-grid,.identity-fields { grid-template-columns:1fr; }
.identity-fields .agent-field:last-child { grid-column:1; }
.policy-lists .policy-transfer-actions { flex-direction:row; }
.policy-lists .policy-transfer-actions x-icon { transform:rotate(90deg); }
.identity-fields .agent-field.wide { grid-column:1; }
.policy-filters { grid-template-columns:1fr; }
.policy-list { min-height:20rem; }
.agent-manager-card { grid-template-columns:3rem minmax(0,1fr); }

View file

@ -128,58 +128,50 @@
</button>
<div class="agent-profile-dropdown" x-show="showProfileDropdown" x-transition.opacity style="display: none;">
<button class="model-switcher-item agent-profile-create" @click="
window.openAgentEditor?.({ view: 'create', contextId: $store.chats?.selected || '' });
showProfileDropdown = false;
">
<x-icon style="font-size: 15px;" name="add"></x-icon>
<span>Create agent</span>
</button>
<button class="model-switcher-item agent-profile-edit" @click="
window.openAgentEditor?.({
view: 'edit',
profileId: $store.chats.selectedContext.agent_profile,
contextId: $store.chats?.selected || ''
});
showProfileDropdown = false;
">
<x-icon style="font-size: 15px;" name="edit"></x-icon>
<span>Edit agent</span>
</button>
<div class="model-switcher-divider" style="opacity:0.2;"></div>
<template x-if="$store.modelConfig.agentProfilesLoading">
<div class="model-switcher-item disabled">Loading profiles...</div>
</template>
<template x-for="profile in $store.modelConfig.getAgentProfileList($store.chats.selectedContext.agent_profile, $store.chats.selectedContext.agent_profile_label)" :key="profile.key">
<div class="model-switcher-item agent-profile-item"
:class="{
'active': profile.key === $store.chats.selectedContext.agent_profile,
'disabled': $store.chats.selectedContext.running || $store.modelConfig.agentProfileSaving
}"
@click="
if (profile.key === $store.chats.selectedContext.agent_profile) {
<div class="agent-profile-row"
:class="{ 'active': profile.key === $store.chats.selectedContext.agent_profile }">
<button type="button" class="model-switcher-item agent-profile-item"
:disabled="$store.chats.selectedContext.running || $store.modelConfig.agentProfileSaving"
:class="{ 'disabled': $store.chats.selectedContext.running || $store.modelConfig.agentProfileSaving }"
@click="
if (profile.key === $store.chats.selectedContext.agent_profile) {
showProfileDropdown = false;
} else {
$store.modelConfig.selectAgentProfile($store.chats?.selected || '', profile.key)
.then(ok => { if (ok) showProfileDropdown = false; });
}
">
<span class="agent-profile-avatar" :style="`background:${$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).color}`">
<img x-show="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" :src="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" alt="">
<span x-show="!$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" x-text="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).initials"></span>
</span>
<div class="model-switcher-preset-name" x-text="profile.label || profile.key"></div>
</button>
<button type="button" class="agent-profile-row-edit"
:aria-label="`Edit ${profile.label || profile.key}`"
@click="
showProfileDropdown = false;
} else {
$store.modelConfig.selectAgentProfile($store.chats?.selected || '', profile.key)
.then(ok => { if (ok) showProfileDropdown = false; });
}
">
<span class="agent-profile-avatar" :style="`background:${$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).color}`">
<img x-show="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" :src="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" alt="">
<span x-show="!$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" x-text="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).initials"></span>
</span>
<div class="model-switcher-preset-name" x-text="profile.label || profile.key"></div>
window.openAgentEditor?.({
view: 'edit',
profileId: profile.key,
contextId: $store.chats?.selected || ''
});
">
<x-icon name="edit"></x-icon>
<span>Edit</span>
</button>
</div>
</template>
<div class="model-switcher-divider" style="opacity:0.2;"></div>
<button class="model-switcher-item agent-profile-settings" @click="
window.openAgentEditor?.({ view: 'manage', contextId: $store.chats?.selected || '' });
showProfileDropdown = false;
window.openAgentEditor?.({ view: 'manage', contextId: $store.chats?.selected || '' });
">
<x-icon style="font-size: 14px;" name="manage_accounts"></x-icon>
<span>Manage agents</span>
@ -316,10 +308,56 @@
.model-switcher-item.active {
background: color-mix(in srgb, var(--color-highlight) 12%, transparent);
}
.agent-profile-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
border-radius: 6px;
transition: background 0.1s ease;
}
.agent-profile-row:hover {
background: var(--color-background-hover, rgba(255,255,255,0.06));
}
.agent-profile-row.active {
background: color-mix(in srgb, var(--color-highlight) 12%, transparent);
}
.agent-profile-item {
display: flex;
align-items: center;
gap: 7px;
width: 100%;
min-width: 0;
border: 0;
background: transparent;
color: var(--color-text);
text-align: left;
}
.agent-profile-row .agent-profile-item:hover {
background: transparent;
}
.agent-profile-row-edit {
display: flex;
align-items: center;
gap: 4px;
margin-right: 4px;
min-height: 24px;
padding: 4px 6px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--color-text);
font: inherit;
font-size: 0.72rem;
cursor: pointer;
opacity: 0.72;
white-space: nowrap;
}
.agent-profile-row-edit:hover {
background: color-mix(in srgb, var(--color-text) 8%, transparent);
opacity: 1;
}
.agent-profile-row-edit x-icon {
font-size: 0.8rem;
}
.model-switcher-item.revert {
display: flex;
@ -388,17 +426,6 @@
text-align: left;
font-family: inherit;
}
.agent-profile-create {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
border: none;
background: transparent;
color: var(--color-text);
font-family: inherit;
font-weight: 500;
}
@media (max-width: 600px) {
.model-switcher-label {

View file

@ -1,6 +1,14 @@
import { callJsonApi, fetchApi } from "/js/api.js";
const API_BASE = "/plugins/_model_config";
const BUILT_IN_AGENT_COLORS = {
agent0: "#8E44AD",
default: "#D35400",
developer: "#202124",
hacker: "#C0392B",
researcher: "#6C5CE7",
"tiny-local": "#E67E22",
};
function normalizeModelIdentity(value) {
if (!value || typeof value !== "object") return null;
const provider = String(value.provider || "").trim();
@ -127,7 +135,9 @@ export const switcherMethods = {
for (const char of profileKey || label) hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
return {
url: profile.avatarUrl || "",
color: profile.avatar?.kind === "color" ? profile.avatar.value : palette[hash % palette.length],
color: profile.avatar?.kind === "color"
? profile.avatar.value
: BUILT_IN_AGENT_COLORS[profileKey] || palette[hash % palette.length],
initials: label.trim().split(/\s+/).slice(0, 2).map(word => word[0]).join("").toUpperCase() || "A",
};
},

View file

@ -520,6 +520,26 @@ def test_remove_my_changes_preserves_manual_and_unknown_files(
assert json.loads(tool_config.read_text()) == {"manual": True}
def test_destructive_cleanup_deletes_only_its_enumerated_plan(
user_root: Path,
) -> None:
root = user_root / "researcher"
planned_files = _write_manual_files(root)
agent_yaml = root / "agent.yaml"
agent_yaml.write_text("title: Mine\n", encoding="utf-8")
planned_files[agent_yaml] = agent_yaml.read_bytes()
plan = editor.plan_remove_changes("researcher", destructive=True)
assert set(plan.changes) == set(planned_files)
unplanned = root / "created-after-plan.txt"
unplanned.write_text("keep", encoding="utf-8")
editor.apply_change_plan(plan)
assert all(not path.exists() for path in planned_files)
assert unplanned.read_text(encoding="utf-8") == "keep"
def test_mixed_save_matches_plan_preserves_every_unrelated_family_and_refreshes_cache(
user_root: Path,
monkeypatch: pytest.MonkeyPatch,

View file

@ -27,10 +27,30 @@ SWITCHER_MIXIN = ROOT / "plugins" / "_model_config" / "webui" / "switcher-mixin.
def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls() -> None:
modal = MODAL.read_text(encoding="utf-8")
switcher = SWITCHER.read_text(encoding="utf-8")
tool_section = re.search(
r'data-agent-editor-section="3".*?(?=<section x-show="\$store\.agentEditor\.section === \'4\'")',
modal,
re.DOTALL,
).group(0)
skill_section = re.search(
r'data-agent-editor-section="4".*?(?=<section x-show="\$store\.agentEditor\.section === \'5\'")',
modal,
re.DOTALL,
).group(0)
easy_surface = modal.split('<div class="agent-advanced"', 1)[0]
assert "Create agent" in switcher
assert "Edit agent" in switcher
assert "Create agent" not in switcher
assert "Manage agents" in switcher
assert '<div class="agent-profile-row"' in switcher
assert 'class="agent-profile-row-edit"' in switcher
assert ':aria-label="`Edit ${profile.label || profile.key}`"' in switcher
assert "profileId: profile.key" in switcher
assert "<span>Edit</span>" in switcher
assert "profile.customized" not in switcher
assert 'class="model-switcher-item agent-profile-edit"' not in switcher
assert "min-height: 24px" in re.search(
r"\.agent-profile-row-edit\s*\{([^}]*)\}", switcher
).group(1)
assert "createAgentProfileChat" not in switcher
assert all(
label in modal
@ -39,34 +59,108 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
"Prompt files",
"Tools",
"Skills",
"Review & test",
"Standard tools — recommended",
"No optional tools",
"Custom selection",
"Review",
"Save & test",
)
)
assert 'aria-label="Editor mode"' in modal
assert "Allow selected" in modal and "Block selected" in modal
assert "No optional tools" not in modal
assert 'class="agent-model-preset-picker"' in modal
assert 'x-model="$store.agentEditor.draft.modelPreset"' in modal
assert '`Use current preset (${$store.agentEditor.state.model_preset.effective})`' in modal
assert 'x-for="preset in $store.agentEditor.state.model_presets"' in modal
assert "Edit Presets" in modal
assert "Manage presets" not in modal
assert "model-preset-row" not in modal
assert 'class="easy-tool-details"' not in modal
assert 'x-for="tool in $store.agentEditor.toolCatalog"' in easy_surface
assert ':checked="$store.agentEditor.isToolAllowed(tool)"' in easy_surface
assert "$store.agentEditor.setEasyToolAllowed(tool.id, $event.target.checked)" in easy_surface
assert "Choose tools in Advanced" not in modal
assert "To enable or disable skills, click Advanced." in easy_surface
assert 'x-for="skill in $store.agentEditor' not in easy_surface
assert 'class="easy-tool-actions"' not in modal
assert 'class="policy-editor" :disabled="$store.agentEditor.draft.toolPolicy.mode !== \'custom\'"' in tool_section
assert 'class="policy-editor" :disabled="$store.agentEditor.draft.skillPolicy.mode !== \'custom\'"' in skill_section
assert 'class="policy-lists"' in tool_section and 'class="policy-lists"' in skill_section
assert 'aria-label="Block selected tools"' in tool_section
assert 'aria-label="Allow selected tools"' in tool_section
assert 'aria-label="Block selected skills"' in skill_section
assert 'aria-label="Allow selected skills"' in skill_section
assert '<details class="policy-description"' not in tool_section
assert '<details class="policy-description"' not in skill_section
assert tool_section.count('class="policy-item-description"') == 2
assert skill_section.count('class="policy-item-description"') == 2
assert "Your changes override the built-in profile. The original files stay unchanged." in modal
assert "This project has a higher-priority tool policy." in modal
assert "Unavailable · retained" in modal
assert "Edit / Create override" in modal
assert "This project has different tool settings." in modal
assert "Unavailable — kept in your settings" in modal
assert "Customize this file" not in modal
assert 'role="tablist" aria-label="Prompt view"' in modal
assert "No prompt files match your search." in modal
assert "Saving will change exactly these files — nothing else." in modal
assert "Review & test" not in modal
assert "Refresh change plan" not in modal
assert "Back to Easy" not in modal
assert "This agent has a detailed prompt" not in modal
assert "Replace with simple instructions" not in modal
assert "easyInstructionsEditable" not in modal
assert '<textarea id="agent-editor-instructions"' in modal
assert "<h2" not in modal
assert "Section 1" not in modal
assert '<textarea id="agent-editor-description" rows="2"' in modal
assert "When new tools are installed later" in modal
assert "When new skills are installed later" in modal
assert "Block until reviewed" in modal
assert "No blocked tools" in modal and "No blocked skills" in modal
assert "policy-description" not in modal and "-webkit-line-clamp:2" not in modal
assert 'class="prompt-file-list" role="region" aria-label="Prompt file list" tabindex="0"' in modal
assert 'class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0"' in modal
assert 'width:1.5rem; height:1.5rem' in modal
assert "moveAllVisibleTools(false)" in modal and "moveAllVisibleSkills(false)" in modal
assert "!$store.agentEditor.draft.creating && $store.agentEditor.dirty" in modal
assert ':aria-invalid=' in modal
assert modal.count('role="alert"') >= 4
assert "Fix ${$store.agentEditor.validationIssues().length}" in modal
assert "Delete all customizations for this profile" in modal
assert 'input[type="checkbox"]' in modal and "appearance:none" in modal
assert 'promptDisplayState(prompt)' in modal
assert 'promptSourceChain($store.agentEditor.selectedPromptDraft)' in modal
assert "Will reset to inherited on save." in modal
assert ':title="prompt.filename"' not in modal
assert "promptEditPending($store.agentEditor.selectedPromptDraft)" in modal
assert 'aria-label="Discard current edit"' in modal
assert 'aria-label="Accept current edit"' in modal
assert ':readonly="!$store.agentEditor.isPromptEditing' not in modal
assert ".prompt-pane textarea:focus-visible { outline-offset:-2px; }" in modal
assert all(
label in STORE.read_text(encoding="utf-8")
for label in (
"Model preset",
"Project references",
"Active sessions",
"Profile content",
"Projects using this agent",
"Open chats using this agent",
"Saved settings",
)
)
assert "agent-profile-avatar" in switcher
assert '<button type="button" class="model-switcher-item agent-profile-item"' in switcher
assert '<div class="model-switcher-item agent-profile-item"' not in switcher
switcher_mixin = SWITCHER_MIXIN.read_text(encoding="utf-8")
assert "avatar_url" in switcher_mixin
assert "BUILT_IN_AGENT_COLORS" in switcher_mixin
assert "customized: !!profile.has_user_overrides" not in switcher_mixin
assert 'name="palette"' in modal and 'name="add_photo_alternate"' in modal
assert ".easy-tool-summary" not in modal
assert "grid-template-columns:minmax(0,1fr) 2.5rem minmax(0,1fr)" in modal
assert ".policy-lists .policy-transfer-actions { flex-direction:row; }" in modal
store_source = STORE.read_text(encoding="utf-8")
assert "easyToolsOpen" not in store_source
assert "easySkills" not in store_source
assert "get toolMode" not in store_source
assert "get easyTools" not in store_source
assert "firstSentence" not in store_source
assert '.agent-editor [aria-invalid="true"]' not in modal
assert ".field-error { display:block; color:var(--color-text-secondary)" in modal
assert 'callJsonApi("/plugins/_agent_editor/agent_editor"' in switcher_mixin
assert "@keydown.ctrl.s.prevent" in modal
assert "@media (max-width: 760px)" in modal
@ -75,6 +169,7 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
def test_agent_editor_store_has_no_conversational_or_model_builder_path() -> None:
source = STORE.read_text(encoding="utf-8")
modal = MODAL.read_text(encoding="utf-8")
switcher_source = (
ROOT / "plugins" / "_model_config" / "webui" / "switcher-mixin.js"
).read_text(encoding="utf-8")
@ -84,10 +179,7 @@ def test_agent_editor_store_has_no_conversational_or_model_builder_path() -> Non
assert "a0-create-agent" not in switcher_source
assert "save_agent_data" not in source
assert not re.search(r"utility.?model|call.?model|generate", source, re.IGNORECASE)
assert all(
f"setMode('advanced', '{section}')" in MODAL.read_text(encoding="utf-8")
for section in ("1", "2", "3")
)
assert "Advanced <span" not in modal
@pytest.mark.skipif(not shutil.which("node"), reason="node is required")
@ -96,6 +188,7 @@ def test_local_slugging_and_fresh_chat_profile_selection_are_deterministic() ->
source = re.sub(r"^import .*?;\n", "", source, flags=re.MULTILINE)
harness = r"""
const calls = [];
const confirmations = [];
const createStore = (_name, value) => value;
const callJsonApi = async (endpoint, payload) => {
calls.push({ endpoint, payload });
@ -104,14 +197,22 @@ const callJsonApi = async (endpoint, payload) => {
const fetchApi = async () => ({ ok: true, json: async () => ({}) });
const closeModal = async () => {};
const openModal = async () => {};
const showConfirmDialog = async () => true;
const showConfirmDialog = async options => { confirmations.push(options); return false; };
const chatsStore = {
selected: "old-chat",
selectChat: async (id) => calls.push({ endpoint: "selectChat", payload: id }),
};
const modelConfigStore = { loadAgentProfiles: async () => {} };
const modelConfigStore = {
loadAgentProfiles: async () => {},
getAgentProfileVisual: (_id, label) => ({ color: "#123456", url: "", initials: label?.[0] || "A" }),
};
globalThis.window = globalThis;
globalThis.document = { dispatchEvent: (event) => calls.push({ endpoint: "event", payload: event.type }) };
globalThis.document = {
dispatchEvent: (event) => calls.push({ endpoint: "event", payload: event.type }),
createElement: () => ({ textContent: "", get innerHTML() { return this.textContent; } }),
addEventListener: () => {},
removeEventListener: () => {},
};
globalThis.CustomEvent = class { constructor(type) { this.type = type; } };
globalThis.sessionStorage = { setItem: () => {}, getItem: () => "", removeItem: () => {} };
globalThis.localStorage = { setItem: () => {}, getItem: () => "" };
@ -120,6 +221,18 @@ globalThis.requestAnimationFrame = callback => callback();
checks = r"""
if (slugifyProfileName(" Crème Brûlée__Lab ") !== "creme-brulee-lab") throw new Error("slug mismatch");
if (slugifyProfileName("東京") !== "") throw new Error("unsupported slug mismatch");
store.draft = { title: "stale" };
store.initialDraft = { title: "clean" };
store.intent = { view: "manage", contextId: "" };
const modalTitle = { textContent: "" };
const modalElement = { querySelector: selector => selector === ".modal-title" ? modalTitle : null };
const modalInner = { classList: { toggle: () => {} } };
await store.mount({
closest: selector => selector === ".modal" ? modalElement : selector === ".modal-inner" ? modalInner : null,
querySelector: () => null,
});
if (store.draft !== null || store.initialDraft !== null || store.dirty || store.loading) throw new Error("manage mount kept stale draft state");
if (modalTitle.textContent !== "Manage agents") throw new Error("manage mount title mismatch");
store.state = {
profile: { id: "new-agent", avatar_url: "", metadata: { title: {}, description: {}, context: {}, avatar: {} } },
prompts: [
@ -131,26 +244,97 @@ store.state = {
skills: { policy: { mode: "inherit" }, has_override: false, catalog: [] },
};
store.makeDraft(true);
store.state.tools.catalog = [{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true }];
if (await store.previewPlan()) throw new Error("invalid plan unexpectedly succeeded");
if (store.planStatus !== "blocked" || store.error || store.validationIssues().length !== 2) throw new Error("blocked plan state mismatch");
if (store.fieldIssue("name")?.message !== "Agent name is required.") throw new Error("inline name issue missing");
await store.save();
if (store.error) throw new Error("validation leaked into dismissible error banner");
store.state.profile.id = "new-agent";
store.state.profile.origin = "Built-in";
store.state.profile.metadata.title = { inherited_source: "agents/new-agent/agent.yaml" };
if (store.metadataProvenance("title") !== "Using the default") throw new Error("default provenance mismatch");
store.profiles = [{ id: "researcher", title: "Researcher" }];
store.state.profile.metadata.title = { inherited_source: "agents/researcher/agent.yaml" };
if (store.metadataProvenance("title") !== "Inherited from Researcher") throw new Error("inherited provenance mismatch");
store.state.profile.metadata.title.has_override = true;
if (store.metadataProvenance("title") !== "Customized by you") throw new Error("custom provenance mismatch");
store.state.tools.catalog = [
{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
{ id: "local:gone", name: "gone", label: "Gone", origin: "Unavailable", available: false },
];
store.draft.toolPolicy = { mode: "custom", default: "allow", allowed: [], blocked: ["local:shell"] };
if (JSON.stringify(store.skillWarnings({ allowed_tools: ["shell"] })) !== JSON.stringify(["shell"])) throw new Error("live skill warning missing");
if (store.toolCatalog.length !== 1 || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("Easy custom tool state mismatch");
await store.moveAllVisibleTools(true);
if (confirmations.at(-1)?.title !== "Allow 1 shown tool?" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("filtered bulk confirmation mismatch");
confirmations.length = 0;
store.selectedAllowedTools = ["local:shell"];
store.useStandardTools();
if (store.draft.toolPolicy.mode !== "inherit" || !store.isToolAllowed(store.state.tools.catalog[0]) || store.filteredTools(true).length !== 1 || store.selectedAllowedTools.length) throw new Error("standard tool state mismatch");
store.setEasyToolAllowed("local:shell", false);
if (store.draft.toolPolicy.mode !== "custom" || store.draft.toolPolicy.default !== "allow" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("Easy uncheck did not block tool");
store.setEasyToolAllowed("local:shell", true);
if (store.draft.toolPolicy.mode !== "inherit" || !store.isToolAllowed(store.state.tools.catalog[0]) || store.draft.toolPolicy.blocked.length) throw new Error("Easy recheck did not restore standard access");
store.draft.toolPolicy = { mode: "custom", default: "block", allowed: [], blocked: [] };
store.setEasyToolAllowed("local:shell", true);
if (!store.isToolAllowed(store.state.tools.catalog[0]) || !store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("Easy check ignored block-by-default policy");
store.setEasyToolAllowed("local:shell", false);
if (store.isToolAllowed(store.state.tools.catalog[0]) || store.draft.toolPolicy.allowed.length) throw new Error("Easy uncheck ignored block-by-default policy");
store.useStandardTools();
store.chooseTools();
if (store.draft.toolPolicy.mode !== "custom" || store.draft.toolPolicy.default !== "allow" || store.section !== "3") throw new Error("custom tool editor did not open");
store.state.skills.catalog = [
{ name: "Research", path: "skills/research/SKILL.md", origin: "Agent Zero", description: "Research sources", available: true, tags: [], allowed_tools: [] },
{ name: "Gone", path: "skills/gone/SKILL.md", origin: "Unavailable", description: "Missing skill", available: false, tags: [], allowed_tools: [] },
];
store.draft.skillPolicy = { mode: "custom", default: "allow", allowed: [], blocked: ["Research"] };
if (store.skillCatalog.length !== 1 || store.filteredSkills(false).length !== 1 || store.filteredSkills(true).length !== 1) throw new Error("custom skill catalog mismatch");
store.selectedBlockedSkills = ["Research"];
store.useStandardSkills();
if (store.draft.skillPolicy.mode !== "inherit" || store.filteredSkills(true).length !== 1 || store.filteredSkills(false).length || store.selectedBlockedSkills.length) throw new Error("standard skill summary mismatch");
store.chooseSkills();
if (store.draft.skillPolicy.mode !== "custom" || store.draft.skillPolicy.default !== "allow") throw new Error("custom skill editor did not open");
store.draft.title = "Preserved Agent";
store.onNameInput();
store.instructions.value = "Preserved instructions";
store.setEasyToolMode("off");
store.draft.creating = false;
store.instructions.value = "";
if (store.fieldIssue("instructions")) throw new Error("existing empty instructions were rejected");
store.instructions.value = "Preserved instructions";
store.markPromptSet("agent.system.main.specifics.md");
if (store.instructions.reset || store.promptEditPending(store.instructions)) throw new Error("Easy instructions did not update its edit checkpoint");
store.restoreInstructions();
if (!store.instructions.reset || store.instructions.value !== "" || store.promptEditPending(store.instructions)) throw new Error("default instructions were not restored");
store.draft.creating = true;
store.instructions.value = "Preserved instructions";
store.instructions.reset = false;
const communication = store.draft.prompts["agent.system.main.communication.md"];
if (store.isPromptEditing(communication.filename) || store.promptDisplayState(communication) !== "Inherited") throw new Error("inherited prompt was editable");
store.beginPromptEdit(communication.filename);
if (store.filteredPromptFiles("2.4")[0] !== communication) throw new Error("grouped prompt filter mismatch");
if (store.promptEditPending(communication) || store.promptDisplayState(communication) !== "Default") throw new Error("default prompt checkpoint mismatch");
communication.value += "\nNew rule";
store.markPromptSet(communication.filename);
if (store.promptDisplayState(communication) !== "Overridden here") throw new Error("override state missing");
if (store.promptSourceChain(communication) !== "Framework → Researcher → Your override") throw new Error("override chain missing");
store.onPromptInput(communication.filename);
if (!store.promptEditPending(communication)) throw new Error("prompt edit actions did not appear");
if (!store.buildPatch().prompts.set[communication.filename].endsWith("New rule")) throw new Error("pending prompt edit missing from sparse patch");
store.discardPromptEdit(communication.filename);
if (store.promptEditPending(communication) || communication.value !== "Inherited comm") throw new Error("prompt edit was not discarded");
if (store.buildPatch().prompts.set[communication.filename]) throw new Error("discarded prompt edit remained in sparse patch");
communication.value += "\nNew rule";
store.onPromptInput(communication.filename);
store.acceptPromptEdit(communication.filename);
if (store.promptEditPending(communication)) throw new Error("prompt edit was not accepted");
if (store.promptDisplayState(communication) !== "Customized by you") throw new Error("customized state missing");
if (store.promptSourceChain(communication) !== "Customized by you") throw new Error("customized provenance missing");
store.resetPrompt(communication.filename);
if (store.isPromptEditing(communication.filename) || store.promptDisplayState(communication) !== "Reset to inherited") throw new Error("reset state mismatch");
if (store.promptEditPending(communication) || store.promptDisplayState(communication) !== "Will use the default") throw new Error("reset state mismatch");
const draftBeforeModes = JSON.stringify(store.draft);
store.setMode("advanced", "2");
store.setMode("easy");
if (store.section !== "2" || JSON.stringify(store.draft) !== draftBeforeModes) throw new Error("mode switch lost draft");
store.setMode("advanced", "5");
await Promise.resolve();
await Promise.resolve();
if (store.planStatus !== "ready" || calls.at(-1).payload.action !== "plan") throw new Error("review plan was not computed on entry");
calls.length = 0;
store.intent = { contextId: "source-chat" };
await store.openFreshChat("researcher", true);
const endpoints = calls.map((item) => item.endpoint);
@ -159,6 +343,14 @@ if (JSON.stringify(endpoints) !== JSON.stringify(expected)) throw new Error(JSON
if (calls[1].payload.agent_profile !== "researcher") throw new Error("profile not selected");
if (calls[2].payload.action !== "clear") throw new Error("chat preset override not cleared");
if (store.readyNoteContext !== "fresh-chat") throw new Error("ready note missing");
await store.planRemoval(true);
if (!store.pendingMutation?.destructive || store.section !== "5" || store.planStatus !== "ready") throw new Error("removal plan was replaced");
if (calls.at(-1).payload.action !== "plan_remove_changes") throw new Error("removal plan request missing");
store.plan = { written: ["usr/agents/researcher/agent.yaml"], deleted: ["usr/agents/researcher/prompts/old.md"], warnings: [] };
await store.applyPendingMutation();
if (confirmations.length !== 1 || confirmations[0].type !== "danger") throw new Error("danger confirmation missing");
if (!confirmations[0].message.includes("agent.yaml") || !confirmations[0].message.includes("old.md")) throw new Error("planned paths missing from confirmation");
if (confirmations[0].title !== "Delete all customizations for this profile?") throw new Error("cleanup confirmation title mismatch");
"""
module_source = harness + "\n" + source + "\n" + checks
module_url = "data:text/javascript;base64," + base64.b64encode(