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.

Alessandro committed Aug 8, 2026 at 06:48 UTC 457e92584fd609f0f4496933d62cd9abc64eaccf
7 files changed +783 -343
plugins/_agent_editor/AGENTS.md
+12
@@ -27,6 +27,18 @@
27 provenance and shown as higher priority; the editor still writes only the
28 user-profile scope.
29 - Bundled `agents/` files are read-only.
30 +- Advanced prompt text is directly editable; per-file close/check actions
31 + discard or accept the current edit checkpoint, while the editor's global save
32 + remains the only persistence boundary.
33 +- The configurable tool catalog is visible in both modes; Easy provides direct
34 + allow/block checkboxes and points to Advanced for skill access. Skills remain
35 + Advanced-only. Advanced keeps both complete selectors visible but disabled
36 + for inherited access and interactive for custom access. Framework-required
37 + tools remain absent from the tool catalog.
38 +- Model selection reuses `_model_config`'s compact preset dropdown and preset
39 + editor; Agent Editor persists only the scoped preset reference.
40 +- The WebUI uses the shared modal stack, labeled prompt scroll regions, and
41 + 24px-or-larger policy and text-action targets.
42
43 ## Verification
44
plugins/_agent_editor/webui/agent-editor-store.js
+216 -84
@@ -41,11 +41,6 @@ function policyFromState(value, hasOverride) {
41 return normalized;
42 }
43
44 -function easyToolMode(policy) {
45 - if (policy.mode !== "custom") return "inherit";
46 - return policy.default === "block" && policy.allowed.length === 0 ? "off" : "custom";
47 -}
48 -
44 function policyAllows(policy, id) {
45 if (policy.mode !== "custom") return true;
46 if (policy.blocked.includes(id)) return false;
@@ -81,7 +76,6 @@ const model = {
76 draft: null,
77 initialDraft: null,
78 selectedPrompt: SPECIFICS,
84 - promptGroup: "2.1",
79 promptFileSearch: "",
80 promptTextSearch: "",
81 comparePrompt: "",
@@ -94,10 +88,10 @@ const model = {
88 skillOrigin: "all",
89 selectedAllowedSkills: [],
90 selectedBlockedSkills: [],
97 - editingPrompts: [],
98 - easyToolsOpen: false,
91 + promptEditBaselines: {},
92 plan: { written: [], deleted: [], warnings: [] },
93 planLoading: false,
94 + planStatus: "idle",
95 pendingMutation: null,
96 readyNoteContext: "",
97 suppressClosePrompt: false,
@@ -125,15 +119,35 @@ const model = {
119 async mount(root) {
120 this.revokePreview();
121 this.root = root;
122 + this.draft = null;
123 + this.initialDraft = null;
124 + this.promptEditBaselines = {};
125 + this.loading = true;
126 this.error = "";
127 this.pendingMutation = null;
128 this.plan = { written: [], deleted: [], warnings: [] };
129 + this.planStatus = "idle";
130 this.view = this.intent.view === "manage" ? "manage" : "editor";
131 + const modalTitle =
132 + this.view === "manage"
133 + ? "Manage agents"
134 + : this.intent.view === "create"
135 + ? "Create agent"
136 + : "Edit agent";
137 + this.setModalTitle(modalTitle);
138 + const modal = this.root.closest(".modal");
139 + const titleAfterLoad = (event) => {
140 + if (event.detail?.modal?.element !== modal) return;
141 + document.removeEventListener("modal-content-loaded", titleAfterLoad);
142 + this.setModalTitle(modalTitle);
143 + };
144 + document.addEventListener("modal-content-loaded", titleAfterLoad);
145 this.mode = "easy";
146 this.section = this.savedSection();
147 this.syncSurface();
148 await this.loadProfiles();
149 if (this.view === "manage") {
150 + this.loading = false;
151 this.setModalTitle("Manage agents");
152 return;
153 }
@@ -226,16 +240,18 @@ const model = {
240 };
241 this.initialDraft = clone(this.draft);
242 this.selectedPrompt = SPECIFICS;
229 - this.promptGroup = "2.1";
243 this.comparePrompt = "";
231 - this.easyToolsOpen = false;
244 + this.planStatus = "idle";
245 this.selectedAllowedTools = [];
246 this.selectedBlockedTools = [];
247 this.selectedAllowedSkills = [];
248 this.selectedBlockedSkills = [];
236 - this.editingPrompts = Object.values(prompts)
237 - .filter((prompt) => prompt.has_override || prompt.filename === SPECIFICS)
238 - .map((prompt) => prompt.filename);
249 + this.promptEditBaselines = Object.fromEntries(
250 + Object.values(prompts).map((prompt) => [prompt.filename, {
251 + value: prompt.value,
252 + reset: prompt.reset,
253 + }]),
254 + );
255 },
256
257 get dirty() {
@@ -255,8 +271,8 @@ const model = {
271 return this.draft?.prompts?.[SPECIFICS] || null;
272 },
273
258 - get toolMode() {
259 - return this.draft ? easyToolMode(this.draft.toolPolicy) : "inherit";
274 + get toolCatalog() {
275 + return (this.state?.tools?.catalog || []).filter((item) => item.available !== false);
276 },
277
278 get toolOrigins() {
@@ -267,6 +283,10 @@ const model = {
283 return unique((this.state?.skills?.catalog || []).map((item) => item.origin)).sort();
284 },
285
286 + get skillCatalog() {
287 + return (this.state?.skills?.catalog || []).filter((item) => item.available !== false);
288 + },
289 +
290 get promptGroups() {
291 const groups = new Map();
292 for (const prompt of Object.values(this.draft?.prompts || {})) {
@@ -312,9 +332,10 @@ const model = {
332 inner?.classList.toggle("agent-editor-easy", this.mode !== "advanced");
333 },
334
315 - setMode(mode, section = "") {
335 + setMode(mode, section = "", preview = true) {
336 this.mode = mode === "advanced" ? "advanced" : "easy";
317 - if (section) this.setSection(section);
337 + if (section) this.setSection(section, preview);
338 + else if (preview && this.mode === "advanced" && this.section === "5") this.previewPlan();
339 this.syncSurface();
340 if (this.mode === "advanced") {
341 requestAnimationFrame(() => {
@@ -323,12 +344,12 @@ const model = {
344 }
345 },
346
326 - setSection(section) {
347 + setSection(section, preview = true) {
348 this.section = String(section || "1");
349 try {
350 localStorage.setItem(LAST_SECTION_KEY, this.section);
351 } catch {}
331 - if (this.section === "5") this.previewPlan();
352 + if (preview && this.section === "5") this.previewPlan();
353 },
354
355 savedSection() {
@@ -354,18 +375,21 @@ const model = {
375 return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "A";
376 },
377
357 - fallbackColor() {
358 - const source = this.draft?.profileId || this.draft?.title || "agent";
359 - const palette = ["#6C5CE7", "#0984E3", "#00A884", "#D35400", "#C0392B", "#8E44AD"];
360 - let hash = 0;
361 - for (const char of source) hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
362 - return palette[hash % palette.length];
378 + profileVisual(profile = {}) {
379 + const id = String(profile.id || profile.key || "");
380 + const title = String(profile.title || profile.label || id || "Agent");
381 + const visual = modelConfigStore.getAgentProfileVisual(id, title);
382 + return {
383 + ...visual,
384 + color: profile.avatar?.kind === "color" ? profile.avatar.value : visual.color,
385 + url: profile.avatar_url || visual.url,
386 + };
387 },
388
389 avatarColor() {
390 return this.draft?.avatar?.kind === "color"
391 ? this.draft.avatar.value
368 - : this.fallbackColor();
392 + : this.profileVisual({ id: this.draft?.profileId, title: this.draft?.title }).color;
393 },
394
395 chooseAvatarColor(value) {
@@ -443,12 +467,13 @@ const model = {
467
468 metadataProvenance(key) {
469 const metadata = this.state?.profile?.metadata?.[key] || {};
446 - if (this.metadataResetPending(key)) {
447 - return `Inherited source: ${metadata.inherited_source || "none"}`;
448 - }
449 - return metadata.has_override
450 - ? `Your override · ${metadata.source || "user layer"}`
451 - : `Inherited · ${metadata.source || "no lower value"}`;
470 + if (metadata.has_override && !this.metadataResetPending(key)) return "Customized by you";
471 + const match = String(metadata.inherited_source || metadata.source || "")
472 + .match(/(?:^|\/)agents\/([^/]+)/);
473 + const sourceId = match?.[1] || "";
474 + if (!sourceId || sourceId === this.state?.profile?.id) return "Using the default";
475 + const source = this.profiles.find((profile) => profile.id === sourceId)?.title || sourceId;
476 + return `Inherited from ${source}`;
477 },
478
479 markMetadataSet(key) {
@@ -460,23 +485,46 @@ const model = {
485 if (!prompt) return;
486 prompt.value = String(prompt.inherited || "");
487 prompt.reset = true;
488 + this.acceptPromptEdit(prompt.filename);
489 },
490
491 markPromptSet(filename) {
492 const prompt = this.draft?.prompts?.[filename];
493 if (prompt) {
494 prompt.reset = false;
469 - if (!this.editingPrompts.includes(filename)) this.editingPrompts.push(filename);
495 + this.acceptPromptEdit(filename);
496 }
497 },
498
473 - isPromptEditing(filename) {
474 - return this.editingPrompts.includes(filename);
499 + onPromptInput(filename) {
500 + const prompt = this.draft?.prompts?.[filename];
501 + if (prompt) prompt.reset = false;
502 },
503
477 - beginPromptEdit(filename) {
478 - if (!this.editingPrompts.includes(filename)) this.editingPrompts.push(filename);
479 - requestAnimationFrame(() => this.root?.querySelector("#agent-editor-prompt-text")?.focus());
504 + promptEditPending(prompt) {
505 + const baseline = this.promptEditBaselines[prompt?.filename];
506 + return Boolean(
507 + prompt
508 + && baseline
509 + && (prompt.value !== baseline.value || prompt.reset !== baseline.reset),
510 + );
511 + },
512 +
513 + acceptPromptEdit(filename) {
514 + const prompt = this.draft?.prompts?.[filename];
515 + if (!prompt) return;
516 + this.promptEditBaselines[filename] = {
517 + value: prompt.value,
518 + reset: prompt.reset,
519 + };
520 + },
521 +
522 + discardPromptEdit(filename) {
523 + const prompt = this.draft?.prompts?.[filename];
524 + const baseline = this.promptEditBaselines[filename];
525 + if (!prompt || !baseline) return;
526 + prompt.value = baseline.value;
527 + prompt.reset = baseline.reset;
528 },
529
530 resetPrompt(filename) {
@@ -484,19 +532,26 @@ const model = {
532 if (!prompt) return;
533 prompt.value = prompt.inherited;
534 prompt.reset = true;
487 - this.editingPrompts = this.editingPrompts.filter((item) => item !== filename);
535 + this.acceptPromptEdit(filename);
536 },
537
538 promptDisplayState(prompt) {
491 - if (prompt?.reset) return "Reset to inherited";
492 - if (this.promptDirty(prompt)) return prompt.value === "" ? "Overridden here (empty)" : "Overridden here";
493 - return prompt?.state || "Unavailable";
539 + if (prompt?.reset) return "Will use the default";
540 + if (prompt?.has_override || this.promptDirty(prompt)) return "Customized by you";
541 + return prompt?.state === "Unavailable" ? "Unavailable" : "Default";
542 },
543
544 promptSourceChain(prompt) {
497 - const chain = [...(prompt?.source_chain || [])].filter((item) => item !== "Your override");
498 - if (!prompt?.reset && (prompt?.has_override || this.promptDirty(prompt))) chain.push("Your override");
499 - return chain.join(" → ") || "No inherited source";
545 + if (!prompt?.reset && (prompt?.has_override || this.promptDirty(prompt))) return "Customized by you";
546 + const source = [...(prompt?.source_chain || [])]
547 + .filter((item) => item !== "Your override")
548 + .at(-1);
549 + const current = String(
550 + this.state?.profile?.metadata?.title?.effective || this.state?.profile?.id || "",
551 + );
552 + return !source || source.toLowerCase() === current.toLowerCase()
553 + ? "Default"
554 + : `Inherited from ${source}`;
555 },
556
557 selectPrompt(filename) {
@@ -506,10 +561,10 @@ const model = {
561 this.comparePrompt = "";
562 },
563
509 - filteredPromptFiles() {
564 + filteredPromptFiles(group = "") {
565 const query = this.promptFileSearch.trim().toLowerCase();
566 return Object.values(this.draft?.prompts || {}).filter((prompt) =>
512 - prompt.group === this.promptGroup && (!query || [prompt.filename, prompt.state, prompt.source]
567 + (!group || prompt.group === group) && (!query || [prompt.filename, prompt.state, prompt.source]
568 .join(" ").toLowerCase().includes(query)),
569 );
570 },
@@ -546,12 +601,10 @@ const model = {
601 globalThis.justToast?.("Path copied", "success", 1200, "agent-editor-copy");
602 },
603
549 - setEasyToolMode(mode) {
550 - if (mode === "inherit") {
551 - this.draft.toolPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
552 - } else if (mode === "off") {
553 - this.draft.toolPolicy = { mode: "custom", default: "block", allowed: [], blocked: [] };
554 - }
604 + useStandardTools() {
605 + this.draft.toolPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
606 + this.selectedAllowedTools = [];
607 + this.selectedBlockedTools = [];
608 },
609
610 chooseTools() {
@@ -561,6 +614,28 @@ const model = {
614 this.setMode("advanced", "3");
615 },
616
617 + setEasyToolAllowed(id, allow) {
618 + if (this.draft.toolPolicy.mode !== "custom") {
619 + this.draft.toolPolicy = { mode: "custom", default: "allow", allowed: [], blocked: [] };
620 + }
621 + this.moveTools([id], allow);
622 + const policy = this.draft.toolPolicy;
623 + if (this.initialDraft?.toolPolicy.mode !== "custom" && policy.default === "allow"
624 + && !policy.allowed.length && !policy.blocked.length) this.useStandardTools();
625 + },
626 +
627 + useStandardSkills() {
628 + this.draft.skillPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
629 + this.selectedAllowedSkills = [];
630 + this.selectedBlockedSkills = [];
631 + },
632 +
633 + chooseSkills() {
634 + if (this.draft.skillPolicy.mode !== "custom") {
635 + this.draft.skillPolicy = { mode: "custom", default: "allow", allowed: [], blocked: [] };
636 + }
637 + },
638 +
639 setPolicyDefault(kind, nextDefault) {
640 const policy = kind === "tool" ? this.draft.toolPolicy : this.draft.skillPolicy;
641 const catalog = kind === "tool" ? this.state.tools.catalog : this.state.skills.catalog;
@@ -585,6 +660,7 @@ const model = {
660 filteredTools(allowed) {
661 const query = this.toolSearch.trim().toLowerCase();
662 return (this.state?.tools?.catalog || []).filter((item) => {
663 + if (this.draft.toolPolicy.mode !== "custom" && item.available === false) return false;
664 if (this.isToolAllowed(item) !== allowed) return false;
665 const category = item.id.split(":", 1)[0];
666 if (this.toolCategory !== "all" && category !== this.toolCategory) return false;
@@ -601,12 +677,17 @@ const model = {
677 },
678
679 moveAllVisibleTools(allow) {
604 - this.moveTools(this.filteredTools(!allow).map((item) => item.id), allow);
680 + return this.confirmBulkMove(
681 + "tool",
682 + this.filteredTools(!allow).map((item) => item.id),
683 + allow,
684 + );
685 },
686
687 filteredSkills(allowed) {
688 const query = this.skillSearch.trim().toLowerCase();
689 return (this.state?.skills?.catalog || []).filter((item) => {
690 + if (this.draft.skillPolicy.mode !== "custom" && item.available === false) return false;
691 if (this.isSkillAllowed(item) !== allowed) return false;
692 if (this.skillOrigin !== "all" && item.origin !== this.skillOrigin) return false;
693 return !query || [item.name, item.description, item.origin, ...(item.tags || [])]
@@ -621,7 +702,26 @@ const model = {
702 },
703
704 moveAllVisibleSkills(allow) {
624 - this.moveSkills(this.filteredSkills(!allow).map((item) => item.name), allow);
705 + return this.confirmBulkMove(
706 + "skill",
707 + this.filteredSkills(!allow).map((item) => item.name),
708 + allow,
709 + );
710 + },
711 +
712 + async confirmBulkMove(kind, ids, allow) {
713 + if (!ids.length) return;
714 + const action = allow ? "Allow" : "Block";
715 + const plural = `${kind}${ids.length === 1 ? "" : "s"}`;
716 + const confirmed = await showConfirmDialog({
717 + title: `${action} ${ids.length} shown ${plural}?`,
718 + message: `<p>This changes every ${kind} currently shown by your filters.</p>`,
719 + confirmText: `${action} shown ${plural}`,
720 + type: "warning",
721 + });
722 + if (!confirmed) return;
723 + if (kind === "tool") this.moveTools(ids, allow);
724 + else this.moveSkills(ids, allow);
725 },
726
727 skillWarnings(skill) {
@@ -647,20 +747,44 @@ const model = {
747 }
748 },
749
650 - validationErrors() {
651 - const errors = [];
652 - if (!this.draft?.title.trim()) errors.push("Agent name is required.");
750 + validationIssues() {
751 + const issues = [];
752 + if (!this.draft?.title.trim()) {
753 + issues.push({ key: "name", section: "1", field: "agent-editor-advanced-name", label: "Agent name", message: "Agent name is required." });
754 + }
755 if (this.draft?.creating) {
654 - if (!this.draft.profileId || !PROFILE_ID.test(this.draft.profileId)) {
655 - errors.push("Enter a name that produces a valid profile ID.");
756 + if (this.draft.title.trim() && (!this.draft.profileId || !PROFILE_ID.test(this.draft.profileId))) {
757 + issues.push({ key: "name", section: "1", field: "agent-editor-advanced-name", label: "Agent name", message: "Enter a name that produces a valid profile ID." });
758 + }
759 + if (this.profileConflict) {
760 + 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.` });
761 + }
762 + if (!this.instructions?.value.trim()) {
763 + issues.push({ key: "instructions", section: "2", field: "agent-editor-prompt-text", label: "Instructions", message: "Instructions are required for a new agent." });
764 }
657 - if (this.profileConflict) errors.push(`An agent with profile ID ${this.draft.profileId} already exists.`);
658 - if (!this.instructions?.value.trim()) errors.push("Instructions are required for a new agent.");
659 - } else if (this.mode === "easy" && !this.instructions?.value.trim()) {
660 - errors.push("Instructions can’t be empty. To remove your changes, use Restore original instructions.");
765 }
662 - if (this.avatarUploading) errors.push("Wait for the avatar upload to finish.");
663 - return errors;
766 + if (this.avatarUploading) {
767 + issues.push({ key: "avatar", section: "1", field: "agent-editor-advanced-name", label: "Avatar", message: "Wait for the avatar upload to finish." });
768 + }
769 + return issues;
770 + },
771 +
772 + validationErrors() {
773 + return this.validationIssues().map((issue) => issue.message);
774 + },
775 +
776 + sectionIssues(section) {
777 + return this.validationIssues().filter((issue) => issue.section === String(section));
778 + },
779 +
780 + fieldIssue(key) {
781 + return this.validationIssues().find((issue) => issue.key === key) || null;
782 + },
783 +
784 + showValidationIssue(issue) {
785 + if (!issue) return;
786 + if (issue.key === "instructions") this.selectPrompt(SPECIFICS);
787 + this.setMode("advanced", issue.section);
788 },
789
790 buildPatch() {
@@ -701,12 +825,9 @@ const model = {
825 : { mode: "inherit" };
826 }
827 if (!same(this.draft.toolPolicy, this.initialDraft.toolPolicy)) {
704 - const mode = easyToolMode(this.draft.toolPolicy);
705 - patch.tool_policy = mode === "inherit"
828 + patch.tool_policy = this.draft.toolPolicy.mode === "inherit"
829 ? { mode: "inherit" }
707 - : mode === "off"
708 - ? { mode: "off" }
709 - : clone(this.draft.toolPolicy);
830 + : clone(this.draft.toolPolicy);
831 }
832 if (!same(this.draft.skillPolicy, this.initialDraft.skillPolicy)) {
833 patch.skill_policy = this.draft.skillPolicy.mode === "inherit"
@@ -720,11 +841,13 @@ const model = {
841 if (!this.draft) return false;
842 const errors = this.validationErrors();
843 if (errors.length) {
723 - this.error = errors[0];
844 + this.error = "";
845 this.plan = { written: [], deleted: [], warnings: [] };
846 + this.planStatus = "blocked";
847 return false;
848 }
849 this.planLoading = true;
850 + this.planStatus = "loading";
851 this.error = "";
852 this.pendingMutation = null;
853 try {
@@ -734,8 +857,10 @@ const model = {
857 context_id: this.intent.contextId,
858 });
859 this.plan = data;
860 + this.planStatus = "ready";
861 return true;
862 } catch (error) {
863 + this.planStatus = "error";
864 this.error = error.message || String(error);
865 return false;
866 } finally {
@@ -747,7 +872,7 @@ const model = {
872 if (this.saving || !this.draft) return false;
873 const errors = this.validationErrors();
874 if (errors.length) {
750 - this.error = errors[0];
875 + this.error = "";
876 return false;
877 }
878 this.saving = true;
@@ -829,8 +954,9 @@ const model = {
954 context_id: this.intent.contextId,
955 });
956 this.plan = data;
957 + this.planStatus = "ready";
958 this.pendingMutation = { destructive };
833 - this.setMode("advanced", "5");
959 + this.setMode("advanced", "5", false);
960 } catch (error) {
961 this.error = error.message || String(error);
962 } finally {
@@ -840,12 +966,18 @@ const model = {
966
967 async applyPendingMutation() {
968 if (!this.pendingMutation) return;
843 - const count = (this.plan.written?.length || 0) + (this.plan.deleted?.length || 0);
969 + const planned = [
970 + ["Will update", this.plan.written || []],
971 + ["Will delete", this.plan.deleted || []],
972 + ].filter(([, paths]) => paths.length);
973 + const changes = planned.length
974 + ? planned.map(([label, paths]) => `<p><strong>${label}</strong></p><ul>${paths.map((path) => `<li><code>${escapeHtml(path)}</code></li>`).join("")}</ul>`).join("")
975 + : "<p>No files will change.</p>";
976 const confirmed = await showConfirmDialog({
845 - title: this.pendingMutation.destructive ? "Delete the entire user override?" : "Remove my changes?",
846 - message: `${count} planned file change${count === 1 ? "" : "s"}. Bundled files are not touched.`,
847 - confirmText: "Apply",
848 - type: this.pendingMutation.destructive ? "danger" : "warning",
977 + title: this.pendingMutation.destructive ? "Delete all customizations for this profile?" : "Remove my changes?",
978 + message: `${changes}<p>Agent Zero’s defaults are not touched.</p>`,
979 + confirmText: this.pendingMutation.destructive ? "Delete planned files" : "Remove planned changes",
980 + type: "danger",
981 });
982 if (!confirmed) return;
983 try {
@@ -857,7 +989,7 @@ const model = {
989 });
990 this.pendingMutation = null;
991 await this.loadEditor(this.draft.profileId);
860 - globalThis.justToast?.("Your agent overrides were removed.", "success", 2200);
992 + globalThis.justToast?.("Your agent customizations were removed.", "success", 2200);
993 } catch (error) {
994 this.error = error.message || String(error);
995 }
@@ -872,7 +1004,7 @@ const model = {
1004 });
1005 const confirmed = await showConfirmDialog({
1006 title: `Delete ${escapeHtml(profileId)}?`,
875 - message: `${this.deletionImpactHtml(data)}<p>This removes only the custom user profile and cannot be undone.</p>`,
1007 + message: `${this.deletionImpactHtml(data)}<p>This permanently removes this custom agent.</p>`,
1008 confirmText: "Delete agent",
1009 type: "danger",
1010 });
@@ -904,9 +1036,9 @@ const model = {
1036 return [
1037 `<p><strong>Files</strong>${list(impact.files || data?.deleted || [], "None")}</p>`,
1038 `<p><strong>Model preset</strong><br>${escapeHtml(impact.model_preset || "None")}</p>`,
907 - `<p><strong>Project references</strong>${list(impact.project_references, "None found")}</p>`,
908 - `<p><strong>Active sessions</strong>${list(impact.active_sessions, "None found")}</p>`,
909 - `<p><strong>Profile content</strong><br>${escapeHtml(contents.length ? contents.join(", ") : "No tools, extensions, skills, assets, or plugin data")}</p>`,
1039 + `<p><strong>Projects using this agent</strong>${list(impact.project_references, "None found")}</p>`,
1040 + `<p><strong>Open chats using this agent</strong>${list(impact.active_sessions, "None found")}</p>`,
1041 + `<p><strong>Saved settings</strong><br>${escapeHtml(contents.length ? contents.join(", ") : "No additional settings or assets")}</p>`,
1042 ].join("");
1043 },
1044
plugins/_agent_editor/webui/main.html
+227 -180
@@ -25,32 +25,26 @@
25 </template>
26
27 <template x-if="!$store.agentEditor.loading && $store.agentEditor.view === 'manage'">
28 - <section class="agent-manager" aria-labelledby="agent-manager-title">
29 - <div class="agent-manager-heading">
30 - <div>
31 - <h2 id="agent-manager-title">Manage agents</h2>
32 - <p>Built-in profiles stay read-only. Your edits live as sparse overrides.</p>
33 - </div>
34 - <button type="button" class="button primary" @click="$store.agentEditor.loadEditor('new-agent', true)">
35 - <x-icon name="add"></x-icon><span>Create agent</span>
36 - </button>
37 - </div>
28 + <section class="agent-manager" aria-label="Manage agents">
29 + <p class="agent-manager-intro">Edits to built-in agents are saved as your own changes — originals are never modified.</p>
30 <div class="agent-manager-list">
31 <template x-for="profile in $store.agentEditor.profiles" :key="profile.id">
32 <article class="agent-manager-card">
41 - <div class="agent-manager-avatar" :style="`background:${profile.avatar?.kind === 'color' ? profile.avatar.value : '#586174'}`">
42 - <img x-show="profile.avatar_url" :src="profile.avatar_url" :alt="`${profile.title || profile.id} avatar`">
43 - <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>
33 + <div class="agent-manager-avatar" :style="`background:${$store.agentEditor.profileVisual(profile).color}`">
34 + <img x-show="$store.agentEditor.profileVisual(profile).url" :src="$store.agentEditor.profileVisual(profile).url" :alt="`${profile.title || profile.id} avatar`">
35 + <span x-show="!$store.agentEditor.profileVisual(profile).url" x-text="$store.agentEditor.profileVisual(profile).initials"></span>
36 </div>
37 <div class="agent-manager-copy">
38 <div class="agent-manager-name">
39 <strong x-text="profile.title || profile.id"></strong>
40 <span class="agent-origin" x-text="profile.origin"></span>
49 - <span class="agent-change-dot" x-show="profile.has_user_overrides" title="Includes your changes" aria-label="Includes your changes">●</span>
41 + <span class="agent-status-badge is-customized" x-show="profile.has_user_overrides">
42 + <x-icon name="edit_note"></x-icon><span>Customized by you</span>
43 + </span>
44 </div>
45 <p x-text="profile.description || 'No description'"></p>
46 <code x-text="profile.id"></code>
53 - <div class="agent-project-notice" x-show="profile.project_override_active">Project override is currently higher priority.</div>
47 + <div class="agent-project-notice" x-show="profile.project_override_active">This project’s customization takes priority here.</div>
48 </div>
49 <div class="agent-manager-actions">
50 <button type="button" class="button" @click="$store.agentEditor.loadEditor(profile.id, false)"><x-icon name="edit"></x-icon>Edit</button>
@@ -65,11 +59,10 @@
59 <template x-if="!$store.agentEditor.loading && $store.agentEditor.view === 'editor' && $store.agentEditor.draft">
60 <div class="agent-editor-workspace">
61 <header class="agent-editor-topbar">
68 - <div class="agent-editor-heading">
62 + <div class="agent-editor-heading" x-show="$store.agentEditor.intent.view === 'manage' || (!$store.agentEditor.draft.creating && $store.agentEditor.dirty)">
63 <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>
70 - <div>
71 - <h2 x-text="$store.agentEditor.title"></h2>
72 - <div class="agent-editor-subtitle" x-show="$store.agentEditor.dirty"><span class="dirty-dot">●</span> Unsaved changes</div>
64 + <div class="agent-editor-subtitle" x-show="$store.agentEditor.dirty">
65 + <span class="agent-status-badge is-unsaved"><x-icon name="edit_note"></x-icon><span>Unsaved changes</span></span>
66 </div>
67 </div>
68 <div class="agent-mode-switch" role="group" aria-label="Editor mode">
@@ -87,78 +80,64 @@
80 <div class="avatar-progress" x-show="$store.agentEditor.avatarUploading" aria-label="Uploading avatar"><x-icon class="spinning" name="progress_activity"></x-icon></div>
81 </div>
82 <div class="agent-avatar-actions">
90 - <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>
91 - <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>
83 + <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>
84 + <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>
85 <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>
86 </div>
87 </div>
88 <label class="agent-field agent-name-field">
89 <span class="agent-field-label">Agent name</span>
97 - <input id="agent-editor-name" type="text" x-model="$store.agentEditor.draft.title" @input="$store.agentEditor.onNameInput(); $store.agentEditor.markMetadataSet('title')" required autocomplete="off">
90 + <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">
91 </label>
99 - <button type="button" class="agent-advanced-link identity-link" @click="$store.agentEditor.setMode('advanced', '1')">Advanced <span aria-hidden="true">›</span></button>
100 - <div class="agent-id-feedback" x-show="$store.agentEditor.draft.creating && $store.agentEditor.draft.title && (!$store.agentEditor.draft.profileId || $store.agentEditor.profileConflict)">
101 - <template x-if="!$store.agentEditor.draft.profileId">
102 - <span class="field-error">This name cannot produce a supported profile ID.</span>
103 - </template>
104 - <template x-if="$store.agentEditor.profileConflict">
105 - <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>
106 - </template>
92 + <div class="agent-id-feedback" x-show="$store.agentEditor.fieldIssue('name')">
93 + <span id="agent-editor-name-error" class="field-error" role="alert" x-text="$store.agentEditor.fieldIssue('name')?.message"></span>
94 + <button type="button" class="text-button" x-show="$store.agentEditor.profileConflict" @click="$store.agentEditor.openConflictingProfile()">Open existing agent</button>
95 </div>
96 </section>
97
98 <section class="agent-easy-field">
99 <div class="agent-field-heading">
100 <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>
113 - <button type="button" class="agent-advanced-link" @click="$store.agentEditor.setMode('advanced', '2')">Advanced <span aria-hidden="true">›</span></button>
101 </div>
115 - <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>
116 - <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>
102 + <div>
103 + <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>
104 + <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>
105 + <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>
106 + </div>
107 </section>
108
109 <section class="agent-easy-field agent-easy-tools">
110 <div class="agent-field-heading">
121 - <div><div class="agent-field-label">Tools</div></div>
122 - <button type="button" class="agent-advanced-link" @click="$store.agentEditor.setMode('advanced', '3')">Advanced <span aria-hidden="true">›</span></button>
123 - </div>
124 - <div class="easy-tool-summary" role="status" :aria-label="$store.agentEditor.toolMode === 'inherit' ? 'Standard tools' : $store.agentEditor.toolMode === 'off' ? 'No optional tools' : 'Custom selection'">
125 - <div class="tool-state-icon" aria-hidden="true" x-text="$store.agentEditor.toolMode === 'inherit' ? '●' : $store.agentEditor.toolMode === 'off' ? '○' : '◐'"></div>
126 - <div class="easy-tool-copy">
127 - <strong x-text="$store.agentEditor.toolMode === 'inherit' ? 'Standard tools — recommended' : $store.agentEditor.toolMode === 'off' ? 'No optional tools' : 'Custom selection'"></strong>
128 - <span x-text="$store.agentEditor.toolMode === 'inherit' ? 'This agent can use Agent Zero’s standard tools.' : $store.agentEditor.toolMode === 'off' ? 'Optional tools are off for this agent.' : 'This agent uses a custom set of tools.'"></span>
129 - </div>
130 - <button type="button" class="button" x-show="$store.agentEditor.toolMode !== 'custom'" @click="$store.agentEditor.easyToolsOpen = !$store.agentEditor.easyToolsOpen">Change</button>
131 - <button type="button" class="button" x-show="$store.agentEditor.toolMode === 'custom'" @click="$store.agentEditor.setMode('advanced', '3')">Edit in Advanced</button>
111 + <div><div class="agent-field-label">Tools</div><p>Choose which tools this agent can use.</p></div>
112 </div>
133 - <div class="easy-tool-choices" x-show="$store.agentEditor.easyToolsOpen && $store.agentEditor.toolMode !== 'custom'">
134 - <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 Zero’s standard access.</small></span></label>
135 - <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>
136 - <button type="button" class="text-button" @click="$store.agentEditor.chooseTools()">Choose specific tools in Advanced</button>
137 - </div>
138 - <div class="custom-tool-reset" x-show="$store.agentEditor.toolMode === 'custom'">
139 - <span>This removes your custom tool selection.</span>
140 - <button type="button" class="text-button" @click="$store.agentEditor.setEasyToolMode('inherit')">Reset to standard</button>
113 + <div class="easy-tool-list" role="list" aria-label="Tools this agent can use">
114 + <template x-for="tool in $store.agentEditor.toolCatalog" :key="tool.id">
115 + <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>
116 + </template>
117 + <p class="policy-empty" x-show="!$store.agentEditor.toolCatalog.length">No configurable tools are available.</p>
118 </div>
142 - <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>
119 + <p class="easy-skills-hint">To enable or disable skills, click Advanced.</p>
120 + <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>
121 </section>
122 </main>
123
124 <div class="agent-advanced" x-show="$store.agentEditor.mode === 'advanced'">
125 <nav class="agent-advanced-nav" aria-label="Advanced editor sections">
148 - <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">
126 + <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">
127 <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)">
150 - <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>
128 + <span class="section-number" x-text="item.id"></span><span x-text="item.label"></span>
129 + <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>
130 </button>
131 </template>
132 </nav>
133
134 <div class="agent-advanced-content">
135 <section x-show="$store.agentEditor.section === '1'" data-agent-editor-section="1" tabindex="-1" aria-labelledby="agent-section-1-title">
157 - <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>
136 + <header class="advanced-section-heading"><h3 id="agent-section-1-title">Identity & models</h3><p>Set the agent’s identity, delegation guidance, and model preset.</p></header>
137 <div class="origin-row">
138 <span class="agent-origin" x-text="$store.agentEditor.state.profile.origin"></span>
160 - <span class="agent-change-dot" x-show="$store.agentEditor.state.profile.has_user_overrides" title="Includes your changes">● Includes your changes</span>
161 - <span class="agent-project-notice" x-show="$store.agentEditor.state.profile.project_override_active">Project override is currently higher priority.</span>
139 + <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>
140 + <span class="agent-project-notice" x-show="$store.agentEditor.state.profile.project_override_active">This project’s customization takes priority here.</span>
141 </div>
142 <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>
143 <div class="advanced-identity-grid">
@@ -168,107 +147,130 @@
147 <span x-show="$store.agentEditor.draft.avatar?.kind !== 'image' || !$store.agentEditor.draft.avatarPreview" x-text="$store.agentEditor.initials()"></span>
148 </div>
149 <div class="agent-avatar-actions">
171 - <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>
172 - <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>
150 + <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>
151 + <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>
152 <button type="button" class="text-button" @click="$store.agentEditor.resetAvatar()">Reset</button>
153 </div>
154 </div>
155 <div class="identity-fields">
177 - <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>
156 + <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>
157 <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>
179 - <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>
180 - <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>
158 + <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>
159 + <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>
160 </div>
161 </div>
183 - <div class="model-preset-block">
184 - <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>
185 - <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>
186 - <template x-for="preset in $store.agentEditor.state.model_presets" :key="preset.name">
187 - <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>
188 - </template>
162 + <div class="agent-model-preset">
163 + <label class="agent-model-preset-picker">
164 + <span><span class="agent-field-label">Model preset</span><small>Use the current preset or choose another setup for this agent.</small></span>
165 + <select x-model="$store.agentEditor.draft.modelPreset">
166 + <option value="" x-text="`Use current preset (${$store.agentEditor.state.model_preset.effective})`"></option>
167 + <template x-for="preset in $store.agentEditor.state.model_presets" :key="preset.name">
168 + <option :value="preset.name" x-text="preset.name"></option>
169 + </template>
170 + </select>
171 + </label>
172 + <button type="button" class="button" @click="$store.agentEditor.openPresetManager()"><x-icon class="icon" name="tune"></x-icon>Edit Presets</button>
173 </div>
174 </section>
175
176 <section x-show="$store.agentEditor.section === '2'" data-agent-editor-section="2" tabindex="-1" aria-labelledby="agent-section-2-title">
193 - <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>
177 + <header class="advanced-section-heading"><h3 id="agent-section-2-title">Prompt files</h3><p>Customize this agent’s prompt files. You always see the default next to your version.</p></header>
178 <div class="prompt-workspace">
179 <aside class="prompt-browser">
196 - <div class="prompt-groups" role="tablist" aria-label="Prompt groups">
197 - <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>
198 - </div>
180 <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>
200 - <div class="prompt-file-list">
201 - <template x-for="prompt in $store.agentEditor.filteredPromptFiles()" :key="prompt.filename">
202 - <button type="button" :class="{ active: $store.agentEditor.selectedPrompt === prompt.filename }" @click="$store.agentEditor.selectPrompt(prompt.filename)">
203 - <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>
204 - </button>
181 + <div class="prompt-file-list" role="region" aria-label="Prompt file list" tabindex="0">
182 + <template x-for="group in $store.agentEditor.promptGroups" :key="group.id">
183 + <section class="prompt-file-group" x-show="$store.agentEditor.filteredPromptFiles(group.id).length">
184 + <h4 x-text="group.label"></h4>
185 + <template x-for="prompt in $store.agentEditor.filteredPromptFiles(group.id)" :key="prompt.filename">
186 + <button type="button" :class="{ active: $store.agentEditor.selectedPrompt === prompt.filename }" @click="$store.agentEditor.selectPrompt(prompt.filename)">
187 + <span class="prompt-file-name" x-text="prompt.filename"></span><span class="prompt-file-state" x-text="$store.agentEditor.promptDisplayState(prompt)"></span>
188 + </button>
189 + </template>
190 + </section>
191 </template>
192 + <p class="prompt-empty" x-show="!$store.agentEditor.filteredPromptFiles().length">No prompt files match your search.</p>
193 </div>
194 </aside>
208 - <div class="prompt-editor" x-show="$store.agentEditor.selectedPromptDraft">
195 + <div class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0" x-show="$store.agentEditor.selectedPromptDraft">
196 <div class="prompt-editor-header">
197 <div><strong x-text="$store.agentEditor.selectedPrompt"></strong><div class="source-chain" x-text="$store.agentEditor.promptSourceChain($store.agentEditor.selectedPromptDraft)"></div></div>
198 <div class="prompt-actions">
212 - <button type="button" class="button" @click="$store.agentEditor.comparePrompt = 'inherited'">View inherited</button>
213 - <button type="button" class="button" x-show="!$store.agentEditor.isPromptEditing($store.agentEditor.selectedPrompt)" @click="$store.agentEditor.beginPromptEdit($store.agentEditor.selectedPrompt)">Edit / Create override</button>
214 - <button type="button" class="button" @click="$store.agentEditor.comparePrompt = 'compare'">Compare</button>
215 - <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>
216 - <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>
199 + <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>
200 + <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>
201 + <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>
202 + <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>
203 </div>
204 </div>
219 - <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>
205 + <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>
206 <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>
207 + <div class="prompt-view-tabs" role="tablist" aria-label="Prompt view">
208 + <button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === ''" :class="{ active: $store.agentEditor.comparePrompt === '' }" @click="$store.agentEditor.comparePrompt = ''">Your version</button>
209 + <button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === 'inherited'" :class="{ active: $store.agentEditor.comparePrompt === 'inherited' }" @click="$store.agentEditor.comparePrompt = 'inherited'">Default</button>
210 + <button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === 'compare'" :class="{ active: $store.agentEditor.comparePrompt === 'compare' }" @click="$store.agentEditor.comparePrompt = 'compare'">Compare</button>
211 + </div>
212 <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>
213 <div class="prompt-panes" :class="{ compare: $store.agentEditor.comparePrompt === 'compare' }">
223 - <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>
224 - <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>
214 + <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>
215 + <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>
216 </div>
226 - <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>
217 + <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>
218 </div>
219 </div>
220 </section>
221
222 <section x-show="$store.agentEditor.section === '3'" data-agent-editor-section="3" tabindex="-1" aria-labelledby="agent-section-3-title">
232 - <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>
233 - <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>
234 - <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>
235 - <template x-if="$store.agentEditor.draft.toolPolicy.mode === 'custom'"><div>
236 - <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>
237 - <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>
223 + <header class="advanced-section-heading"><h3 id="agent-section-3-title">Tools</h3><p>Choose which tools this agent can use.</p></header>
224 + <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>
225 + <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>
226 + <fieldset class="policy-editor" :disabled="$store.agentEditor.draft.toolPolicy.mode !== 'custom'">
227 + <legend class="sr-only">Tool access selection</legend>
228 + <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>
229 <div class="policy-lists">
239 - <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>
240 - <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>
230 + <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>
231 + <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>
232 + <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>
233 </div>
242 - </div></template>
234 + <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>
235 + </fieldset>
236 </section>
237
238 <section x-show="$store.agentEditor.section === '4'" data-agent-editor-section="4" tabindex="-1" aria-labelledby="agent-section-4-title">
246 - <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>
247 - <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>
248 - <template x-if="$store.agentEditor.draft.skillPolicy.mode === 'custom'"><div>
249 - <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>
250 - <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>
239 + <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>
240 + <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>
241 + <fieldset class="policy-editor" :disabled="$store.agentEditor.draft.skillPolicy.mode !== 'custom'">
242 + <legend class="sr-only">Skill access selection</legend>
243 + <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>
244 <div class="policy-lists">
252 - <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>
253 - <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>
245 + <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>
246 + <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>
247 + <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>
248 </div>
255 - </div></template>
249 + <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>
250 + </fieldset>
251 </section>
252
253 <section x-show="$store.agentEditor.section === '5'" data-agent-editor-section="5" tabindex="-1" aria-labelledby="agent-section-5-title">
259 - <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>
260 - <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>
261 - <div class="change-plan" aria-live="polite">
254 + <header class="advanced-section-heading"><h3 id="agent-section-5-title">Review</h3><p>Saving will change exactly these files — nothing else.</p></header>
255 + <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>
256 + <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>
257 + <div class="review-blocked" role="alert" x-show="$store.agentEditor.planStatus === 'blocked'">
258 + <x-icon name="error"></x-icon>
259 + <div><strong x-text="`Fix ${$store.agentEditor.validationIssues().length} ${$store.agentEditor.validationIssues().length === 1 ? 'issue' : 'issues'} to see the change plan`"></strong>
260 + <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>
261 + </div>
262 + </div>
263 + <div class="change-plan" aria-live="polite" x-show="$store.agentEditor.planStatus === 'ready'">
264 <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>
265 <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>
266 <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>
267 </div>
266 - <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>
268 + <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>
269 <div class="profile-maintenance" x-show="!$store.agentEditor.draft.creating && $store.agentEditor.state.profile.origin !== 'Custom'">
268 - <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>
269 - <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>
270 + <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>
271 + <details><summary>Delete all customizations for this profile</summary><p>This removes every customization saved for this profile after showing each affected file. Agent Zero’s 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>
272 </div>
271 - <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>
273 + <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>
274 </section>
275 </div>
276 </div>
@@ -279,74 +281,89 @@
281 <div class="modal-footer agent-editor-footer" data-modal-footer x-show="!$store.agentEditor.loading">
282 <div class="footer-left">
283 <button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'manage'" @click="window.closeModal?.()">Close</button>
282 - <button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'editor' && $store.agentEditor.mode === 'easy'" @click="window.closeModal?.()">Cancel</button>
283 - <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>
284 + <button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'editor'" @click="window.closeModal?.()">Cancel</button>
285 </div>
286 <div class="footer-actions">
287 <button type="button" class="btn btn-ok" x-show="$store.agentEditor.view === 'manage'" @click="$store.agentEditor.loadEditor('new-agent', true)">Create agent</button>
287 - <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>
288 - <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>
288 + <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>
289 + <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>
290 </div>
291 </div>
292 </div>
293
294 <style>
294 - .agent-editor { color: var(--color-text); min-height: 12rem; }
295 + .agent-editor {
296 + --color-text-secondary: var(--color-text-muted);
297 + --color-error: var(--color-error-text);
298 + --color-warning: var(--color-warning-text);
299 + --agent-editor-action: var(--color-highlight);
300 + --agent-editor-change: var(--color-warning-text);
301 + --agent-editor-danger: var(--color-error-text);
302 + color: var(--color-text);
303 + min-height: 12rem;
304 + min-width: 0;
305 + }
306 .agent-editor h2,.agent-editor h3,.agent-editor h4,.agent-editor p { margin: 0; }
307 .agent-editor button,.agent-editor input,.agent-editor textarea,.agent-editor select { font: inherit; }
297 - .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)); }
308 + .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; }
309 + .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); }
310 + .agent-editor input[type="checkbox"] { display:grid; place-content:center; border-radius:4px; }
311 + .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); }
312 + .agent-editor input[type="checkbox"]:checked { border-color:var(--agent-editor-action); background:var(--agent-editor-action); }
313 + .agent-editor input[type="checkbox"]:checked::before { opacity:1; }
314 + .agent-editor input[type="radio"] { border-radius:50%; }
315 + .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); }
316 + .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)); }
317 .agent-editor-error span { flex:1; white-space:pre-wrap; }
318 .agent-editor-loading { min-height:20rem; display:grid; place-content:center; justify-items:center; gap:.65rem; color:var(--color-text-secondary); }
319 .agent-editor-topbar { display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-bottom:1rem; }
320 .agent-editor-heading { display:flex; align-items:center; gap:.6rem; }
302 - .agent-editor-heading h2,.agent-manager-heading h2 { font-size:1.2rem; }
321 .agent-editor-subtitle { margin-top:.2rem; font-size:.78rem; color:var(--color-text-secondary); }
304 - .dirty-dot,.section-dirty,.agent-change-dot { color:var(--color-accent); }
305 - .agent-mode-switch { display:flex; padding:3px; border:1px solid var(--color-border); border-radius:9px; background:var(--color-input); }
322 + .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; }
323 + .agent-status-badge x-icon { flex:0 0 auto; font-size:.85rem; }
324 + .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); }
325 + .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); }
326 + .agent-status-badge.compact { justify-self:end; padding:.15rem .35rem; font-size:.64rem; }
327 + .agent-mode-switch { display:flex; margin-left:auto; padding:3px; border:1px solid var(--color-border); border-radius:9px; background:var(--color-input); }
328 .agent-mode-switch button { border:0; border-radius:6px; padding:.42rem .8rem; color:var(--color-text-secondary); background:transparent; }
329 .agent-mode-switch button.active { color:var(--color-text); background:var(--color-panel); box-shadow:0 1px 4px rgba(0,0,0,.2); }
330 .agent-easy { max-width:43rem; margin:0 auto; display:flex; flex-direction:column; gap:1.35rem; padding:.25rem .25rem 1rem; }
331 .agent-easy-identity { position:relative; display:grid; grid-template-columns:7.2rem 1fr; gap:1rem; align-items:center; }
310 - .identity-link { position:absolute; top:0; right:0; }
332 .agent-avatar-wrap { display:flex; flex-direction:column; align-items:center; gap:.45rem; }
333 .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); }
334 .agent-avatar img { width:100%; height:100%; object-fit:cover; }
335 .avatar-progress { position:absolute; inset:0; display:grid; place-items:center; background:rgba(0,0,0,.5); }
315 - .agent-avatar-actions { display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:.3rem .55rem; font-size:.76rem; }
316 - .avatar-color-action,.avatar-upload-action { position:relative; cursor:pointer; color:var(--color-accent); }
336 + .agent-avatar-actions { display:flex; align-items:center; justify-content:center; gap:.35rem; font-size:.76rem; white-space:nowrap; }
337 + .avatar-color-action,.avatar-upload-action { position:relative; cursor:pointer; }
338 + .avatar-action-icon { display:grid; place-items:center; width:1.75rem; height:1.75rem; border-radius:6px; color:var(--agent-editor-action); }
339 + .avatar-action-icon x-icon { font-size:1.05rem; }
340 .avatar-color-action input,.avatar-upload-action input { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }
341 .agent-field { display:flex; flex-direction:column; gap:.35rem; min-width:0; }
342 + .agent-name-field { align-self:start; }
343 .agent-field-label { font-weight:650; font-size:.92rem; }
344 .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; }
345 .agent-field small,.agent-field-heading p,.agent-id-feedback,.field-status { color:var(--color-text-secondary); font-size:.79rem; }
346 .field-status { display:block; margin-top:.15rem; }
323 - .field-error { color:var(--color-error); }
347 + .field-error { display:block; color:var(--color-text-secondary); font-size:.79rem; }
348 .agent-id-feedback { grid-column:2; margin-top:-.6rem; }
349 .agent-field-heading { display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; margin-bottom:.5rem; }
326 - .agent-advanced-link,.text-button { border:0; padding:0; background:transparent; color:var(--color-accent); cursor:pointer; font-size:.82rem; text-align:left; }
327 - .agent-advanced-link:hover,.text-button:hover { text-decoration:underline; }
350 + .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; }
351 + .agent-editor .text-button:hover { text-decoration:underline; }
352 .agent-easy textarea { min-height:11rem; resize:vertical; }
353 .restore-action { display:inline-flex; align-items:center; gap:.25rem; margin-top:.4rem; }
330 - .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); }
331 - .tool-state-icon { width:1.15rem; text-align:center; color:var(--color-accent); }
332 - .easy-tool-copy { flex:1; display:flex; flex-direction:column; gap:.15rem; }
333 - .easy-tool-copy span,.custom-tool-reset span { color:var(--color-text-secondary); font-size:.8rem; }
334 - .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; }
335 - .easy-tool-choices label { display:flex; gap:.5rem; align-items:flex-start; }
336 - .easy-tool-choices label span { display:flex; flex-direction:column; }
337 - .easy-tool-choices small { color:var(--color-text-secondary); }
338 - .custom-tool-reset { display:flex; justify-content:space-between; gap:1rem; margin-top:.45rem; padding:0 .2rem; }
354 + .easy-tool-list { display:flex; flex-direction:column; max-height:18rem; overflow-y:auto; padding:.2rem .35rem .2rem 0; }
355 + .easy-tool-list .policy-item { cursor:pointer; }
356 + .easy-skills-hint { margin:.55rem 0 0; color:var(--color-text-secondary); font-size:.79rem; }
357 .agent-advanced { display:grid; grid-template-columns:14rem minmax(0,1fr); gap:1rem; min-height:0; }
358 .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; }
359 .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; }
360 .agent-advanced-nav button.active { color:var(--color-text); background:var(--color-panel); }
361 .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; }
344 - .agent-advanced-content { min-width:0; }
362 + .agent-advanced-content,.agent-editor-workspace,.agent-manager { min-width:0; }
363 .agent-advanced-content > section { outline:none; display:flex; flex-direction:column; gap:1rem; }
346 - .advanced-section-heading { display:flex; justify-content:space-between; gap:1rem; padding-bottom:.8rem; border-bottom:1px solid var(--color-border); }
347 - .advanced-section-heading > div > span { color:var(--color-accent); font-size:.75rem; text-transform:uppercase; letter-spacing:.08em; }
348 - .advanced-section-heading h3 { margin-top:.15rem; font-size:1.2rem; }
349 - .advanced-section-heading p { max-width:36rem; color:var(--color-text-secondary); font-size:.84rem; text-align:right; }
364 + .advanced-section-heading { display:flex; flex-direction:column; gap:.25rem; padding-bottom:.8rem; border-bottom:1px solid var(--color-border); }
365 + .advanced-section-heading h3 { font-size:1.2rem; }
366 + .advanced-section-heading p { max-width:48rem; color:var(--color-text-secondary); font-size:.84rem; }
367 .origin-row { display:flex; flex-wrap:wrap; align-items:center; gap:.6rem; }
368 .built-in-note,.field-provenance { color:var(--color-text-secondary); font-size:.76rem; }
369 .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 @@
371 .advanced-identity-grid { display:grid; grid-template-columns:9rem minmax(0,1fr); gap:1.2rem; }
372 .advanced-avatar { align-self:start; }
373 .identity-fields { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.9rem; }
357 - .identity-fields .agent-field:last-child { grid-column:1/-1; }
358 - .model-preset-block { display:flex; flex-direction:column; gap:.5rem; padding:1rem; border:1px solid var(--color-border); border-radius:10px; }
359 - .model-preset-row { display:flex; gap:.6rem; align-items:flex-start; padding:.6rem; border-radius:8px; cursor:pointer; }
360 - .model-preset-row:hover { background:var(--color-input); }
361 - .model-preset-row > span { display:flex; flex-direction:column; gap:.2rem; min-width:0; }
362 - .model-preset-row small { color:var(--color-text-secondary); overflow-wrap:anywhere; }
363 - .prompt-workspace { display:grid; grid-template-columns:16rem minmax(0,1fr); gap:.8rem; min-height:34rem; }
364 - .prompt-browser { display:flex; flex-direction:column; min-height:0; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
365 - .prompt-groups { display:flex; flex-direction:column; max-height:14rem; overflow:auto; padding:.35rem; border-bottom:1px solid var(--color-border); }
366 - .prompt-groups button { border:0; border-radius:6px; padding:.42rem .5rem; background:transparent; color:var(--color-text-secondary); text-align:left; font-size:.78rem; }
367 - .prompt-groups button.active { background:var(--color-input); color:var(--color-text); }
374 + .identity-fields .agent-field.wide { grid-column:1/-1; }
375 + .agent-model-preset { display:flex; flex-direction:column; align-items:flex-start; gap:.75rem; }
376 + .agent-model-preset-picker { width:100%; display:grid; grid-template-columns:minmax(0,1fr) minmax(13rem,18rem); align-items:center; gap:1rem; margin:0; }
377 + .agent-model-preset-picker > span { display:flex; flex-direction:column; gap:.2rem; min-width:0; }
378 + .agent-model-preset-picker small { color:var(--color-text-secondary); font-size:.79rem; }
379 + .agent-model-preset-picker select { width:100%; }
380 + .agent-model-preset > .button { display:inline-flex; align-items:center; gap:.35rem; }
381 + .prompt-workspace { display:grid; grid-template-columns:minmax(13rem,16rem) minmax(0,1fr); gap:.8rem; min-width:0; height:34rem; }
382 + .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; }
383 .compact-search { display:flex; align-items:center; gap:.3rem; padding:.45rem; border-bottom:1px solid var(--color-border); }
369 - .compact-search input { border:0; background:transparent; outline:none; }
384 + .agent-editor .compact-search input,.agent-editor .policy-search input { min-height:1.8rem; padding:.2rem; border:0; background:transparent; }
385 .prompt-file-list { flex:1; overflow:auto; padding:.35rem; }
386 + .prompt-file-group { min-width:0; margin-bottom:.55rem; }
387 + .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; }
388 .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; }
389 .prompt-file-list button.active { background:var(--color-input); }
390 .prompt-file-name { grid-column:1/-1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:monospace; font-size:.76rem; }
391 .prompt-file-state { color:var(--color-text-secondary); font-size:.7rem; }
375 - .prompt-editor { display:flex; flex-direction:column; gap:.55rem; min-width:0; }
392 + .prompt-empty { padding:.65rem; color:var(--color-text-secondary); font-size:.78rem; }
393 + .prompt-editor { display:flex; flex-direction:column; gap:.55rem; min-width:0; max-width:100%; max-height:100%; overflow:auto; }
394 .prompt-editor-header { display:flex; justify-content:space-between; gap:.7rem; align-items:flex-start; }
395 + .prompt-editor-header > div:first-child { min-width:0; overflow-wrap:anywhere; }
396 .source-chain { margin-top:.2rem; color:var(--color-text-secondary); font-size:.75rem; }
378 - .prompt-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:.35rem; }
379 - .prompt-find { display:flex; align-items:center; gap:.35rem; }
380 - .prompt-find label { flex:1; }
397 + .prompt-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:.35rem; min-width:0; }
398 + .prompt-actions .button { max-width:100%; white-space:normal; overflow-wrap:anywhere; }
399 + .prompt-view-tabs { display:flex; flex-wrap:wrap; gap:.25rem; padding-bottom:.35rem; border-bottom:1px solid var(--color-border); }
400 + .prompt-view-tabs button { min-height:2rem; padding:.35rem .65rem; border:0; border-radius:6px; background:transparent; color:var(--color-text-secondary); }
401 + .prompt-view-tabs button.active { background:var(--color-input); color:var(--color-text); }
402 + .prompt-find { display:flex; flex-wrap:wrap; align-items:center; gap:.35rem; min-width:0; }
403 + .prompt-find label { flex:1 1 10rem; min-width:0; }
404 .prompt-find span { color:var(--color-text-secondary); font-size:.75rem; white-space:nowrap; }
382 - .prompt-panes { display:grid; grid-template-columns:1fr; gap:.65rem; min-height:23rem; }
405 + .prompt-panes { display:grid; grid-template-columns:1fr; gap:.65rem; min-height:18rem; }
406 .prompt-panes.compare { grid-template-columns:1fr 1fr; }
407 .prompt-pane { min-width:0; display:flex; flex-direction:column; gap:.35rem; }
408 .prompt-pane label,.prompt-pane-title { color:var(--color-text-secondary); font-size:.75rem; }
386 - .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; }
409 + .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; }
410 + .prompt-pane textarea:focus-visible { outline-offset:-2px; }
411 .prompt-pane.inherited pre { opacity:.86; }
412 .effective-preview { border:1px solid var(--color-border); border-radius:8px; padding:.55rem .7rem; }
413 + .effective-preview > summary,.profile-maintenance summary { display:flex; align-items:center; min-height:1.5rem; cursor:pointer; }
414 .effective-preview p { margin:.5rem 0; color:var(--color-text-secondary); font-size:.78rem; }
415 .effective-preview pre { min-height:10rem; max-height:25rem; resize:none; }
391 - .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; }
416 + .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; }
417 .policy-mode-row label,.future-default label { display:flex; gap:.35rem; align-items:center; }
393 - .future-default { margin-bottom:.7rem; }
394 - .policy-filters { display:grid; grid-template-columns:minmax(12rem,1fr) auto auto; gap:.6rem; margin-bottom:.7rem; }
418 + .policy-mode-row small { color:var(--color-text-secondary); }
419 + .policy-editor { min-width:0; margin:.75rem 0 0; padding:0; border:0; transition:opacity .15s ease; }
420 + .policy-editor:disabled { opacity:.48; }
421 + .future-default { margin-top:.8rem; }
422 + .future-default legend { padding:0 .35rem; color:var(--color-text-secondary); font-size:.8rem; }
423 + .policy-filters { display:grid; grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto) minmax(8rem,auto); gap:.6rem; margin-bottom:.7rem; }
424 + .policy-filters.skills { grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto); }
425 .policy-filters label { display:flex; align-items:center; gap:.35rem; font-size:.78rem; color:var(--color-text-secondary); }
396 - .policy-lists { display:grid; grid-template-columns:1fr 1fr; gap:.8rem; }
426 + .policy-filters select { flex:1; }
427 + .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); }
428 + .policy-lists { display:grid; grid-template-columns:minmax(0,1fr) 2.5rem minmax(0,1fr); gap:.55rem; }
429 + .policy-transfer-actions { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.5rem; }
430 + .policy-transfer-actions .button { width:2.5rem; height:2.5rem; padding:0; justify-content:center; }
431 .policy-list { display:flex; flex-direction:column; min-width:0; min-height:28rem; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
398 - .policy-list header { display:flex; justify-content:space-between; gap:.5rem; padding:.7rem; border-bottom:1px solid var(--color-border); background:var(--color-input); }
432 + .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); }
433 .policy-list h4 { display:inline; margin-right:.35rem; }
434 .policy-list header span { color:var(--color-text-secondary); font-size:.72rem; }
435 .policy-items { flex:1; max-height:32rem; overflow:auto; padding:.35rem; }
402 - .policy-items label { display:flex; gap:.55rem; align-items:flex-start; padding:.55rem; border-radius:7px; }
403 - .policy-items label:hover { background:var(--color-input); }
404 - .policy-items label > span { display:flex; flex-direction:column; gap:.15rem; min-width:0; }
436 + .policy-item { display:flex; gap:.55rem; align-items:flex-start; padding:.55rem; border-radius:7px; }
437 + .policy-item:hover { background:var(--color-input); }
438 + .policy-item > div { display:flex; flex:1; flex-direction:column; gap:.15rem; min-width:0; }
439 .policy-items small { color:var(--color-text-secondary); overflow-wrap:anywhere; }
440 .policy-items em { color:var(--color-warning); font-size:.72rem; font-style:normal; }
407 - .policy-move { margin:.55rem; justify-content:center; }
441 + .policy-item-description { margin:0; color:var(--color-text-secondary); font-size:.75rem; white-space:pre-wrap; overflow-wrap:anywhere; }
442 + .policy-empty { padding:1rem .7rem; color:var(--color-text-secondary); font-size:.8rem; text-align:center; }
443 + .policy-bulk { min-height:2rem; padding:.25rem .55rem; white-space:normal; }
444 .change-plan { display:grid; grid-template-columns:1fr 1fr; gap:.8rem; }
445 .change-plan section { padding:.85rem; border:1px solid var(--color-border); border-radius:9px; background:var(--color-input); min-width:0; }
446 .change-plan ul { margin:.55rem 0 0; padding-left:1.2rem; }
447 .change-plan code { overflow-wrap:anywhere; }
448 + .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)); }
449 + .review-blocked > x-icon { flex:0 0 auto; color:var(--agent-editor-danger); }
450 + .review-blocked ul { margin:.45rem 0 0; padding-left:1.1rem; }
451 + .review-plan-status { display:flex; flex-wrap:wrap; align-items:center; gap:.45rem; color:var(--color-text-secondary); font-size:.82rem; }
452 + .review-actions { display:flex; flex-wrap:wrap; align-items:center; gap:.65rem; }
453 .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; }
454 .profile-maintenance p { color:var(--color-text-secondary); font-size:.82rem; }
455 .profile-maintenance details { width:100%; padding-top:.6rem; border-top:1px solid var(--color-border); }
456 .profile-maintenance details p { margin:.5rem 0; }
416 - .agent-editor-footer { width:100%; display:flex; justify-content:space-between; gap:.7rem; }
457 + .agent-editor-footer { --agent-editor-action:var(--color-highlight); width:100%; display:flex; justify-content:space-between; gap:.7rem; }
458 .footer-actions { display:flex; gap:.5rem; margin-left:auto; }
459 + .agent-editor-footer .btn-cancel,.agent-editor-secondary-action { border:1px solid var(--color-border); background:transparent; color:var(--color-text); }
460 + .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); }
461 + .agent-editor-secondary-action { display:inline-flex; align-items:center; color:color-mix(in srgb,#fff 82%,var(--agent-editor-action)); }
462 + .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)); }
463 + .agent-editor .button.danger:hover { background:color-mix(in srgb,var(--agent-editor-danger) 15%,var(--color-panel)); }
464 .agent-manager { display:flex; flex-direction:column; gap:1rem; }
419 - .agent-manager-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; }
420 - .agent-manager-heading p,.agent-manager-copy p { color:var(--color-text-secondary); font-size:.82rem; }
465 + .agent-manager-intro,.agent-manager-copy p { color:var(--color-text-secondary); font-size:.82rem; }
466 .agent-manager-list { display:flex; flex-direction:column; gap:.55rem; }
467 .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; }
468 .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 @@
470 .agent-manager-copy { min-width:0; }
471 .agent-manager-name { display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; }
472 .agent-manager-copy code { font-size:.7rem; color:var(--color-text-secondary); }
428 - .agent-manager-actions { display:flex; gap:.4rem; }
473 + .agent-manager-actions { display:flex; flex-wrap:wrap; gap:.4rem; }
474 .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; }
430 - .toast-link { border:0; background:transparent; color:var(--color-accent); text-decoration:underline; cursor:pointer; }
431 - .agent-editor :focus-visible { outline:2px solid var(--color-accent); outline-offset:2px; }
475 + .toast-link { border:0; background:transparent; color:var(--agent-editor-action); text-decoration:underline; cursor:pointer; }
476 + .agent-editor :focus-visible { outline:2px solid var(--agent-editor-action); outline-offset:2px; }
477 .modal-inner.agent-editor-easy { width:min(720px,calc(100vw - 2rem)); max-width:720px; }
478 .modal-inner.agent-editor-advanced { width:calc(100vw - 2rem); max-width:none; height:calc(100vh - 2rem); }
479 .modal-inner.agent-editor-advanced .modal-bd { min-height:0; }
480 .modal-inner.agent-editor-advanced .agent-editor,.modal-inner.agent-editor-advanced .agent-editor-workspace { min-height:100%; }
481 @media (max-width: 760px) {
482 .modal-inner.agent-editor-easy,.modal-inner.agent-editor-advanced { width:100vw; max-width:none; height:100vh; max-height:none; border-radius:0; }
438 - .agent-editor-topbar,.advanced-section-heading { align-items:flex-start; }
439 - .advanced-section-heading { flex-direction:column; }
440 - .advanced-section-heading p { text-align:left; }
483 + .agent-editor-topbar { align-items:flex-start; }
484 .agent-easy-identity { grid-template-columns:1fr; justify-items:center; padding-top:1.8rem; }
485 .agent-name-field { width:100%; }
486 .agent-id-feedback { grid-column:1; width:100%; margin:0; }
487 .agent-advanced { grid-template-columns:1fr; }
488 .agent-advanced-nav { position:static; flex-direction:row; overflow-x:auto; }
489 .agent-advanced-nav button { grid-template-columns:auto auto auto; white-space:nowrap; }
447 - .prompt-workspace { grid-template-columns:1fr; }
448 - .prompt-browser { max-height:22rem; }
490 + .agent-model-preset-picker { grid-template-columns:1fr; gap:.5rem; }
491 + .prompt-workspace { grid-template-columns:1fr; height:auto; }
492 + .prompt-browser { height:22rem; max-height:22rem; }
493 + .prompt-editor { max-height:none; overflow:visible; }
494 .prompt-panes.compare,.policy-lists,.change-plan,.advanced-identity-grid,.identity-fields { grid-template-columns:1fr; }
450 - .identity-fields .agent-field:last-child { grid-column:1; }
495 + .policy-lists .policy-transfer-actions { flex-direction:row; }
496 + .policy-lists .policy-transfer-actions x-icon { transform:rotate(90deg); }
497 + .identity-fields .agent-field.wide { grid-column:1; }
498 .policy-filters { grid-template-columns:1fr; }
499 .policy-list { min-height:20rem; }
500 .agent-manager-card { grid-template-columns:3rem minmax(0,1fr); }
plugins/_model_config/extensions/webui/chat-input-progress-start/model-switcher.html
+78 -51
@@ -128,58 +128,50 @@
128 </button>
129
130 <div class="agent-profile-dropdown" x-show="showProfileDropdown" x-transition.opacity style="display: none;">
131 - <button class="model-switcher-item agent-profile-create" @click="
132 - window.openAgentEditor?.({ view: 'create', contextId: $store.chats?.selected || '' });
133 - showProfileDropdown = false;
134 - ">
135 - <x-icon style="font-size: 15px;" name="add"></x-icon>
136 - <span>Create agent</span>
137 - </button>
138 -
139 - <button class="model-switcher-item agent-profile-edit" @click="
140 - window.openAgentEditor?.({
141 - view: 'edit',
142 - profileId: $store.chats.selectedContext.agent_profile,
143 - contextId: $store.chats?.selected || ''
144 - });
145 - showProfileDropdown = false;
146 - ">
147 - <x-icon style="font-size: 15px;" name="edit"></x-icon>
148 - <span>Edit agent</span>
149 - </button>
150 -
151 - <div class="model-switcher-divider" style="opacity:0.2;"></div>
152 -
131 <template x-if="$store.modelConfig.agentProfilesLoading">
132 <div class="model-switcher-item disabled">Loading profiles...</div>
133 </template>
134
135 <template x-for="profile in $store.modelConfig.getAgentProfileList($store.chats.selectedContext.agent_profile, $store.chats.selectedContext.agent_profile_label)" :key="profile.key">
158 - <div class="model-switcher-item agent-profile-item"
159 - :class="{
160 - 'active': profile.key === $store.chats.selectedContext.agent_profile,
161 - 'disabled': $store.chats.selectedContext.running || $store.modelConfig.agentProfileSaving
162 - }"
163 - @click="
164 - if (profile.key === $store.chats.selectedContext.agent_profile) {
136 + <div class="agent-profile-row"
137 + :class="{ 'active': profile.key === $store.chats.selectedContext.agent_profile }">
138 + <button type="button" class="model-switcher-item agent-profile-item"
139 + :disabled="$store.chats.selectedContext.running || $store.modelConfig.agentProfileSaving"
140 + :class="{ 'disabled': $store.chats.selectedContext.running || $store.modelConfig.agentProfileSaving }"
141 + @click="
142 + if (profile.key === $store.chats.selectedContext.agent_profile) {
143 + showProfileDropdown = false;
144 + } else {
145 + $store.modelConfig.selectAgentProfile($store.chats?.selected || '', profile.key)
146 + .then(ok => { if (ok) showProfileDropdown = false; });
147 + }
148 + ">
149 + <span class="agent-profile-avatar" :style="`background:${$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).color}`">
150 + <img x-show="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" :src="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" alt="">
151 + <span x-show="!$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" x-text="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).initials"></span>
152 + </span>
153 + <div class="model-switcher-preset-name" x-text="profile.label || profile.key"></div>
154 + </button>
155 + <button type="button" class="agent-profile-row-edit"
156 + :aria-label="`Edit ${profile.label || profile.key}`"
157 + @click="
158 showProfileDropdown = false;
166 - } else {
167 - $store.modelConfig.selectAgentProfile($store.chats?.selected || '', profile.key)
168 - .then(ok => { if (ok) showProfileDropdown = false; });
169 - }
170 - ">
171 - <span class="agent-profile-avatar" :style="`background:${$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).color}`">
172 - <img x-show="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" :src="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" alt="">
173 - <span x-show="!$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" x-text="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).initials"></span>
174 - </span>
175 - <div class="model-switcher-preset-name" x-text="profile.label || profile.key"></div>
159 + window.openAgentEditor?.({
160 + view: 'edit',
161 + profileId: profile.key,
162 + contextId: $store.chats?.selected || ''
163 + });
164 + ">
165 + <x-icon name="edit"></x-icon>
166 + <span>Edit</span>
167 + </button>
168 </div>
169 </template>
170
171 <div class="model-switcher-divider" style="opacity:0.2;"></div>
172 <button class="model-switcher-item agent-profile-settings" @click="
181 - window.openAgentEditor?.({ view: 'manage', contextId: $store.chats?.selected || '' });
173 showProfileDropdown = false;
174 + window.openAgentEditor?.({ view: 'manage', contextId: $store.chats?.selected || '' });
175 ">
176 <x-icon style="font-size: 14px;" name="manage_accounts"></x-icon>
177 <span>Manage agents</span>
@@ -316,10 +308,56 @@
308 .model-switcher-item.active {
309 background: color-mix(in srgb, var(--color-highlight) 12%, transparent);
310 }
311 + .agent-profile-row {
312 + display: grid;
313 + grid-template-columns: minmax(0, 1fr) auto;
314 + align-items: center;
315 + border-radius: 6px;
316 + transition: background 0.1s ease;
317 + }
318 + .agent-profile-row:hover {
319 + background: var(--color-background-hover, rgba(255,255,255,0.06));
320 + }
321 + .agent-profile-row.active {
322 + background: color-mix(in srgb, var(--color-highlight) 12%, transparent);
323 + }
324 .agent-profile-item {
325 display: flex;
326 align-items: center;
327 gap: 7px;
328 + width: 100%;
329 + min-width: 0;
330 + border: 0;
331 + background: transparent;
332 + color: var(--color-text);
333 + text-align: left;
334 + }
335 + .agent-profile-row .agent-profile-item:hover {
336 + background: transparent;
337 + }
338 + .agent-profile-row-edit {
339 + display: flex;
340 + align-items: center;
341 + gap: 4px;
342 + margin-right: 4px;
343 + min-height: 24px;
344 + padding: 4px 6px;
345 + border: 0;
346 + border-radius: 4px;
347 + background: transparent;
348 + color: var(--color-text);
349 + font: inherit;
350 + font-size: 0.72rem;
351 + cursor: pointer;
352 + opacity: 0.72;
353 + white-space: nowrap;
354 + }
355 + .agent-profile-row-edit:hover {
356 + background: color-mix(in srgb, var(--color-text) 8%, transparent);
357 + opacity: 1;
358 + }
359 + .agent-profile-row-edit x-icon {
360 + font-size: 0.8rem;
361 }
362 .model-switcher-item.revert {
363 display: flex;
@@ -388,17 +426,6 @@
426 text-align: left;
427 font-family: inherit;
428 }
391 - .agent-profile-create {
392 - display: flex;
393 - align-items: center;
394 - gap: 6px;
395 - width: 100%;
396 - border: none;
397 - background: transparent;
398 - color: var(--color-text);
399 - font-family: inherit;
400 - font-weight: 500;
401 - }
429
430 @media (max-width: 600px) {
431 .model-switcher-label {
plugins/_model_config/webui/switcher-mixin.js
+11 -1
@@ -1,6 +1,14 @@
1 import { callJsonApi, fetchApi } from "/js/api.js";
2
3 const API_BASE = "/plugins/_model_config";
4 +const BUILT_IN_AGENT_COLORS = {
5 + agent0: "#8E44AD",
6 + default: "#D35400",
7 + developer: "#202124",
8 + hacker: "#C0392B",
9 + researcher: "#6C5CE7",
10 + "tiny-local": "#E67E22",
11 +};
12 function normalizeModelIdentity(value) {
13 if (!value || typeof value !== "object") return null;
14 const provider = String(value.provider || "").trim();
@@ -127,7 +135,9 @@ export const switcherMethods = {
135 for (const char of profileKey || label) hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
136 return {
137 url: profile.avatarUrl || "",
130 - color: profile.avatar?.kind === "color" ? profile.avatar.value : palette[hash % palette.length],
138 + color: profile.avatar?.kind === "color"
139 + ? profile.avatar.value
140 + : BUILT_IN_AGENT_COLORS[profileKey] || palette[hash % palette.length],
141 initials: label.trim().split(/\s+/).slice(0, 2).map(word => word[0]).join("").toUpperCase() || "A",
142 };
143 },
tests/test_agent_editor.py
+20
@@ -520,6 +520,26 @@ def test_remove_my_changes_preserves_manual_and_unknown_files(
520 assert json.loads(tool_config.read_text()) == {"manual": True}
521
522
523 +def test_destructive_cleanup_deletes_only_its_enumerated_plan(
524 + user_root: Path,
525 +) -> None:
526 + root = user_root / "researcher"
527 + planned_files = _write_manual_files(root)
528 + agent_yaml = root / "agent.yaml"
529 + agent_yaml.write_text("title: Mine\n", encoding="utf-8")
530 + planned_files[agent_yaml] = agent_yaml.read_bytes()
531 +
532 + plan = editor.plan_remove_changes("researcher", destructive=True)
533 + assert set(plan.changes) == set(planned_files)
534 +
535 + unplanned = root / "created-after-plan.txt"
536 + unplanned.write_text("keep", encoding="utf-8")
537 + editor.apply_change_plan(plan)
538 +
539 + assert all(not path.exists() for path in planned_files)
540 + assert unplanned.read_text(encoding="utf-8") == "keep"
541 +
542 +
543 def test_mixed_save_matches_plan_preserves_every_unrelated_family_and_refreshes_cache(
544 user_root: Path,
545 monkeypatch: pytest.MonkeyPatch,
tests/test_agent_editor_webui.py
+219 -27
@@ -27,10 +27,30 @@ SWITCHER_MIXIN = ROOT / "plugins" / "_model_config" / "webui" / "switcher-mixin.
27 def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls() -> None:
28 modal = MODAL.read_text(encoding="utf-8")
29 switcher = SWITCHER.read_text(encoding="utf-8")
30 + tool_section = re.search(
31 + r'data-agent-editor-section="3".*?(?=<section x-show="\$store\.agentEditor\.section === \'4\'")',
32 + modal,
33 + re.DOTALL,
34 + ).group(0)
35 + skill_section = re.search(
36 + r'data-agent-editor-section="4".*?(?=<section x-show="\$store\.agentEditor\.section === \'5\'")',
37 + modal,
38 + re.DOTALL,
39 + ).group(0)
40 + easy_surface = modal.split('<div class="agent-advanced"', 1)[0]
41
31 - assert "Create agent" in switcher
32 - assert "Edit agent" in switcher
42 + assert "Create agent" not in switcher
43 assert "Manage agents" in switcher
44 + assert '<div class="agent-profile-row"' in switcher
45 + assert 'class="agent-profile-row-edit"' in switcher
46 + assert ':aria-label="`Edit ${profile.label || profile.key}`"' in switcher
47 + assert "profileId: profile.key" in switcher
48 + assert "<span>Edit</span>" in switcher
49 + assert "profile.customized" not in switcher
50 + assert 'class="model-switcher-item agent-profile-edit"' not in switcher
51 + assert "min-height: 24px" in re.search(
52 + r"\.agent-profile-row-edit\s*\{([^}]*)\}", switcher
53 + ).group(1)
54 assert "createAgentProfileChat" not in switcher
55 assert all(
56 label in modal
@@ -39,34 +59,108 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
59 "Prompt files",
60 "Tools",
61 "Skills",
42 - "Review & test",
43 - "Standard tools — recommended",
44 - "No optional tools",
45 - "Custom selection",
62 + "Review",
63 "Save & test",
64 )
65 )
66 assert 'aria-label="Editor mode"' in modal
67 assert "Allow selected" in modal and "Block selected" in modal
68 + assert "No optional tools" not in modal
69 + assert 'class="agent-model-preset-picker"' in modal
70 + assert 'x-model="$store.agentEditor.draft.modelPreset"' in modal
71 + assert '`Use current preset (${$store.agentEditor.state.model_preset.effective})`' in modal
72 + assert 'x-for="preset in $store.agentEditor.state.model_presets"' in modal
73 + assert "Edit Presets" in modal
74 + assert "Manage presets" not in modal
75 + assert "model-preset-row" not in modal
76 + assert 'class="easy-tool-details"' not in modal
77 + assert 'x-for="tool in $store.agentEditor.toolCatalog"' in easy_surface
78 + assert ':checked="$store.agentEditor.isToolAllowed(tool)"' in easy_surface
79 + assert "$store.agentEditor.setEasyToolAllowed(tool.id, $event.target.checked)" in easy_surface
80 + assert "Choose tools in Advanced" not in modal
81 + assert "To enable or disable skills, click Advanced." in easy_surface
82 + assert 'x-for="skill in $store.agentEditor' not in easy_surface
83 + assert 'class="easy-tool-actions"' not in modal
84 + assert 'class="policy-editor" :disabled="$store.agentEditor.draft.toolPolicy.mode !== \'custom\'"' in tool_section
85 + assert 'class="policy-editor" :disabled="$store.agentEditor.draft.skillPolicy.mode !== \'custom\'"' in skill_section
86 + assert 'class="policy-lists"' in tool_section and 'class="policy-lists"' in skill_section
87 + assert 'aria-label="Block selected tools"' in tool_section
88 + assert 'aria-label="Allow selected tools"' in tool_section
89 + assert 'aria-label="Block selected skills"' in skill_section
90 + assert 'aria-label="Allow selected skills"' in skill_section
91 + assert '<details class="policy-description"' not in tool_section
92 + assert '<details class="policy-description"' not in skill_section
93 + assert tool_section.count('class="policy-item-description"') == 2
94 + assert skill_section.count('class="policy-item-description"') == 2
95 assert "Your changes override the built-in profile. The original files stay unchanged." in modal
52 - assert "This project has a higher-priority tool policy." in modal
53 - assert "Unavailable · retained" in modal
54 - assert "Edit / Create override" in modal
96 + assert "This project has different tool settings." in modal
97 + assert "Unavailable — kept in your settings" in modal
98 + assert "Customize this file" not in modal
99 + assert 'role="tablist" aria-label="Prompt view"' in modal
100 + assert "No prompt files match your search." in modal
101 + assert "Saving will change exactly these files — nothing else." in modal
102 + assert "Review & test" not in modal
103 + assert "Refresh change plan" not in modal
104 + assert "Back to Easy" not in modal
105 + assert "This agent has a detailed prompt" not in modal
106 + assert "Replace with simple instructions" not in modal
107 + assert "easyInstructionsEditable" not in modal
108 + assert '<textarea id="agent-editor-instructions"' in modal
109 + assert "<h2" not in modal
110 + assert "Section 1" not in modal
111 + assert '<textarea id="agent-editor-description" rows="2"' in modal
112 + assert "When new tools are installed later" in modal
113 + assert "When new skills are installed later" in modal
114 + assert "Block until reviewed" in modal
115 + assert "No blocked tools" in modal and "No blocked skills" in modal
116 + assert "policy-description" not in modal and "-webkit-line-clamp:2" not in modal
117 + assert 'class="prompt-file-list" role="region" aria-label="Prompt file list" tabindex="0"' in modal
118 + assert 'class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0"' in modal
119 + assert 'width:1.5rem; height:1.5rem' in modal
120 + assert "moveAllVisibleTools(false)" in modal and "moveAllVisibleSkills(false)" in modal
121 + assert "!$store.agentEditor.draft.creating && $store.agentEditor.dirty" in modal
122 + assert ':aria-invalid=' in modal
123 + assert modal.count('role="alert"') >= 4
124 + assert "Fix ${$store.agentEditor.validationIssues().length}" in modal
125 + assert "Delete all customizations for this profile" in modal
126 + assert 'input[type="checkbox"]' in modal and "appearance:none" in modal
127 assert 'promptDisplayState(prompt)' in modal
128 assert 'promptSourceChain($store.agentEditor.selectedPromptDraft)' in modal
129 assert "Will reset to inherited on save." in modal
130 + assert ':title="prompt.filename"' not in modal
131 + assert "promptEditPending($store.agentEditor.selectedPromptDraft)" in modal
132 + assert 'aria-label="Discard current edit"' in modal
133 + assert 'aria-label="Accept current edit"' in modal
134 + assert ':readonly="!$store.agentEditor.isPromptEditing' not in modal
135 + assert ".prompt-pane textarea:focus-visible { outline-offset:-2px; }" in modal
136 assert all(
137 label in STORE.read_text(encoding="utf-8")
138 for label in (
139 "Model preset",
62 - "Project references",
63 - "Active sessions",
64 - "Profile content",
140 + "Projects using this agent",
141 + "Open chats using this agent",
142 + "Saved settings",
143 )
144 )
145 assert "agent-profile-avatar" in switcher
146 + assert '<button type="button" class="model-switcher-item agent-profile-item"' in switcher
147 + assert '<div class="model-switcher-item agent-profile-item"' not in switcher
148 switcher_mixin = SWITCHER_MIXIN.read_text(encoding="utf-8")
149 assert "avatar_url" in switcher_mixin
150 + assert "BUILT_IN_AGENT_COLORS" in switcher_mixin
151 + assert "customized: !!profile.has_user_overrides" not in switcher_mixin
152 + assert 'name="palette"' in modal and 'name="add_photo_alternate"' in modal
153 + assert ".easy-tool-summary" not in modal
154 + assert "grid-template-columns:minmax(0,1fr) 2.5rem minmax(0,1fr)" in modal
155 + assert ".policy-lists .policy-transfer-actions { flex-direction:row; }" in modal
156 + store_source = STORE.read_text(encoding="utf-8")
157 + assert "easyToolsOpen" not in store_source
158 + assert "easySkills" not in store_source
159 + assert "get toolMode" not in store_source
160 + assert "get easyTools" not in store_source
161 + assert "firstSentence" not in store_source
162 + assert '.agent-editor [aria-invalid="true"]' not in modal
163 + assert ".field-error { display:block; color:var(--color-text-secondary)" in modal
164 assert 'callJsonApi("/plugins/_agent_editor/agent_editor"' in switcher_mixin
165 assert "@keydown.ctrl.s.prevent" in modal
166 assert "@media (max-width: 760px)" in modal
@@ -75,6 +169,7 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
169
170 def test_agent_editor_store_has_no_conversational_or_model_builder_path() -> None:
171 source = STORE.read_text(encoding="utf-8")
172 + modal = MODAL.read_text(encoding="utf-8")
173 switcher_source = (
174 ROOT / "plugins" / "_model_config" / "webui" / "switcher-mixin.js"
175 ).read_text(encoding="utf-8")
@@ -84,10 +179,7 @@ def test_agent_editor_store_has_no_conversational_or_model_builder_path() -> Non
179 assert "a0-create-agent" not in switcher_source
180 assert "save_agent_data" not in source
181 assert not re.search(r"utility.?model|call.?model|generate", source, re.IGNORECASE)
87 - assert all(
88 - f"setMode('advanced', '{section}')" in MODAL.read_text(encoding="utf-8")
89 - for section in ("1", "2", "3")
90 - )
182 + assert "Advanced <span" not in modal
183
184
185 @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() ->
188 source = re.sub(r"^import .*?;\n", "", source, flags=re.MULTILINE)
189 harness = r"""
190 const calls = [];
191 +const confirmations = [];
192 const createStore = (_name, value) => value;
193 const callJsonApi = async (endpoint, payload) => {
194 calls.push({ endpoint, payload });
@@ -104,14 +197,22 @@ const callJsonApi = async (endpoint, payload) => {
197 const fetchApi = async () => ({ ok: true, json: async () => ({}) });
198 const closeModal = async () => {};
199 const openModal = async () => {};
107 -const showConfirmDialog = async () => true;
200 +const showConfirmDialog = async options => { confirmations.push(options); return false; };
201 const chatsStore = {
202 selected: "old-chat",
203 selectChat: async (id) => calls.push({ endpoint: "selectChat", payload: id }),
204 };
112 -const modelConfigStore = { loadAgentProfiles: async () => {} };
205 +const modelConfigStore = {
206 + loadAgentProfiles: async () => {},
207 + getAgentProfileVisual: (_id, label) => ({ color: "#123456", url: "", initials: label?.[0] || "A" }),
208 +};
209 globalThis.window = globalThis;
114 -globalThis.document = { dispatchEvent: (event) => calls.push({ endpoint: "event", payload: event.type }) };
210 +globalThis.document = {
211 + dispatchEvent: (event) => calls.push({ endpoint: "event", payload: event.type }),
212 + createElement: () => ({ textContent: "", get innerHTML() { return this.textContent; } }),
213 + addEventListener: () => {},
214 + removeEventListener: () => {},
215 +};
216 globalThis.CustomEvent = class { constructor(type) { this.type = type; } };
217 globalThis.sessionStorage = { setItem: () => {}, getItem: () => "", removeItem: () => {} };
218 globalThis.localStorage = { setItem: () => {}, getItem: () => "" };
@@ -120,6 +221,18 @@ globalThis.requestAnimationFrame = callback => callback();
221 checks = r"""
222 if (slugifyProfileName(" Crème Brûlée__Lab ") !== "creme-brulee-lab") throw new Error("slug mismatch");
223 if (slugifyProfileName("東京") !== "") throw new Error("unsupported slug mismatch");
224 +store.draft = { title: "stale" };
225 +store.initialDraft = { title: "clean" };
226 +store.intent = { view: "manage", contextId: "" };
227 +const modalTitle = { textContent: "" };
228 +const modalElement = { querySelector: selector => selector === ".modal-title" ? modalTitle : null };
229 +const modalInner = { classList: { toggle: () => {} } };
230 +await store.mount({
231 + closest: selector => selector === ".modal" ? modalElement : selector === ".modal-inner" ? modalInner : null,
232 + querySelector: () => null,
233 +});
234 +if (store.draft !== null || store.initialDraft !== null || store.dirty || store.loading) throw new Error("manage mount kept stale draft state");
235 +if (modalTitle.textContent !== "Manage agents") throw new Error("manage mount title mismatch");
236 store.state = {
237 profile: { id: "new-agent", avatar_url: "", metadata: { title: {}, description: {}, context: {}, avatar: {} } },
238 prompts: [
@@ -131,26 +244,97 @@ store.state = {
244 skills: { policy: { mode: "inherit" }, has_override: false, catalog: [] },
245 };
246 store.makeDraft(true);
134 -store.state.tools.catalog = [{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true }];
247 +if (await store.previewPlan()) throw new Error("invalid plan unexpectedly succeeded");
248 +if (store.planStatus !== "blocked" || store.error || store.validationIssues().length !== 2) throw new Error("blocked plan state mismatch");
249 +if (store.fieldIssue("name")?.message !== "Agent name is required.") throw new Error("inline name issue missing");
250 +await store.save();
251 +if (store.error) throw new Error("validation leaked into dismissible error banner");
252 +store.state.profile.id = "new-agent";
253 +store.state.profile.origin = "Built-in";
254 +store.state.profile.metadata.title = { inherited_source: "agents/new-agent/agent.yaml" };
255 +if (store.metadataProvenance("title") !== "Using the default") throw new Error("default provenance mismatch");
256 +store.profiles = [{ id: "researcher", title: "Researcher" }];
257 +store.state.profile.metadata.title = { inherited_source: "agents/researcher/agent.yaml" };
258 +if (store.metadataProvenance("title") !== "Inherited from Researcher") throw new Error("inherited provenance mismatch");
259 +store.state.profile.metadata.title.has_override = true;
260 +if (store.metadataProvenance("title") !== "Customized by you") throw new Error("custom provenance mismatch");
261 +store.state.tools.catalog = [
262 + { id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
263 + { id: "local:gone", name: "gone", label: "Gone", origin: "Unavailable", available: false },
264 +];
265 store.draft.toolPolicy = { mode: "custom", default: "allow", allowed: [], blocked: ["local:shell"] };
266 if (JSON.stringify(store.skillWarnings({ allowed_tools: ["shell"] })) !== JSON.stringify(["shell"])) throw new Error("live skill warning missing");
267 +if (store.toolCatalog.length !== 1 || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("Easy custom tool state mismatch");
268 +await store.moveAllVisibleTools(true);
269 +if (confirmations.at(-1)?.title !== "Allow 1 shown tool?" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("filtered bulk confirmation mismatch");
270 +confirmations.length = 0;
271 +store.selectedAllowedTools = ["local:shell"];
272 +store.useStandardTools();
273 +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");
274 +store.setEasyToolAllowed("local:shell", false);
275 +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");
276 +store.setEasyToolAllowed("local:shell", true);
277 +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");
278 +store.draft.toolPolicy = { mode: "custom", default: "block", allowed: [], blocked: [] };
279 +store.setEasyToolAllowed("local:shell", true);
280 +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");
281 +store.setEasyToolAllowed("local:shell", false);
282 +if (store.isToolAllowed(store.state.tools.catalog[0]) || store.draft.toolPolicy.allowed.length) throw new Error("Easy uncheck ignored block-by-default policy");
283 +store.useStandardTools();
284 +store.chooseTools();
285 +if (store.draft.toolPolicy.mode !== "custom" || store.draft.toolPolicy.default !== "allow" || store.section !== "3") throw new Error("custom tool editor did not open");
286 +store.state.skills.catalog = [
287 + { name: "Research", path: "skills/research/SKILL.md", origin: "Agent Zero", description: "Research sources", available: true, tags: [], allowed_tools: [] },
288 + { name: "Gone", path: "skills/gone/SKILL.md", origin: "Unavailable", description: "Missing skill", available: false, tags: [], allowed_tools: [] },
289 +];
290 +store.draft.skillPolicy = { mode: "custom", default: "allow", allowed: [], blocked: ["Research"] };
291 +if (store.skillCatalog.length !== 1 || store.filteredSkills(false).length !== 1 || store.filteredSkills(true).length !== 1) throw new Error("custom skill catalog mismatch");
292 +store.selectedBlockedSkills = ["Research"];
293 +store.useStandardSkills();
294 +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");
295 +store.chooseSkills();
296 +if (store.draft.skillPolicy.mode !== "custom" || store.draft.skillPolicy.default !== "allow") throw new Error("custom skill editor did not open");
297 store.draft.title = "Preserved Agent";
298 store.onNameInput();
299 store.instructions.value = "Preserved instructions";
140 -store.setEasyToolMode("off");
300 +store.draft.creating = false;
301 +store.instructions.value = "";
302 +if (store.fieldIssue("instructions")) throw new Error("existing empty instructions were rejected");
303 +store.instructions.value = "Preserved instructions";
304 +store.markPromptSet("agent.system.main.specifics.md");
305 +if (store.instructions.reset || store.promptEditPending(store.instructions)) throw new Error("Easy instructions did not update its edit checkpoint");
306 +store.restoreInstructions();
307 +if (!store.instructions.reset || store.instructions.value !== "" || store.promptEditPending(store.instructions)) throw new Error("default instructions were not restored");
308 +store.draft.creating = true;
309 +store.instructions.value = "Preserved instructions";
310 +store.instructions.reset = false;
311 const communication = store.draft.prompts["agent.system.main.communication.md"];
142 -if (store.isPromptEditing(communication.filename) || store.promptDisplayState(communication) !== "Inherited") throw new Error("inherited prompt was editable");
143 -store.beginPromptEdit(communication.filename);
312 +if (store.filteredPromptFiles("2.4")[0] !== communication) throw new Error("grouped prompt filter mismatch");
313 +if (store.promptEditPending(communication) || store.promptDisplayState(communication) !== "Default") throw new Error("default prompt checkpoint mismatch");
314 +communication.value += "\nNew rule";
315 +store.onPromptInput(communication.filename);
316 +if (!store.promptEditPending(communication)) throw new Error("prompt edit actions did not appear");
317 +if (!store.buildPatch().prompts.set[communication.filename].endsWith("New rule")) throw new Error("pending prompt edit missing from sparse patch");
318 +store.discardPromptEdit(communication.filename);
319 +if (store.promptEditPending(communication) || communication.value !== "Inherited comm") throw new Error("prompt edit was not discarded");
320 +if (store.buildPatch().prompts.set[communication.filename]) throw new Error("discarded prompt edit remained in sparse patch");
321 communication.value += "\nNew rule";
145 -store.markPromptSet(communication.filename);
146 -if (store.promptDisplayState(communication) !== "Overridden here") throw new Error("override state missing");
147 -if (store.promptSourceChain(communication) !== "Framework → Researcher → Your override") throw new Error("override chain missing");
322 +store.onPromptInput(communication.filename);
323 +store.acceptPromptEdit(communication.filename);
324 +if (store.promptEditPending(communication)) throw new Error("prompt edit was not accepted");
325 +if (store.promptDisplayState(communication) !== "Customized by you") throw new Error("customized state missing");
326 +if (store.promptSourceChain(communication) !== "Customized by you") throw new Error("customized provenance missing");
327 store.resetPrompt(communication.filename);
149 -if (store.isPromptEditing(communication.filename) || store.promptDisplayState(communication) !== "Reset to inherited") throw new Error("reset state mismatch");
328 +if (store.promptEditPending(communication) || store.promptDisplayState(communication) !== "Will use the default") throw new Error("reset state mismatch");
329 const draftBeforeModes = JSON.stringify(store.draft);
330 store.setMode("advanced", "2");
331 store.setMode("easy");
332 if (store.section !== "2" || JSON.stringify(store.draft) !== draftBeforeModes) throw new Error("mode switch lost draft");
333 +store.setMode("advanced", "5");
334 +await Promise.resolve();
335 +await Promise.resolve();
336 +if (store.planStatus !== "ready" || calls.at(-1).payload.action !== "plan") throw new Error("review plan was not computed on entry");
337 +calls.length = 0;
338 store.intent = { contextId: "source-chat" };
339 await store.openFreshChat("researcher", true);
340 const endpoints = calls.map((item) => item.endpoint);
@@ -159,6 +343,14 @@ if (JSON.stringify(endpoints) !== JSON.stringify(expected)) throw new Error(JSON
343 if (calls[1].payload.agent_profile !== "researcher") throw new Error("profile not selected");
344 if (calls[2].payload.action !== "clear") throw new Error("chat preset override not cleared");
345 if (store.readyNoteContext !== "fresh-chat") throw new Error("ready note missing");
346 +await store.planRemoval(true);
347 +if (!store.pendingMutation?.destructive || store.section !== "5" || store.planStatus !== "ready") throw new Error("removal plan was replaced");
348 +if (calls.at(-1).payload.action !== "plan_remove_changes") throw new Error("removal plan request missing");
349 +store.plan = { written: ["usr/agents/researcher/agent.yaml"], deleted: ["usr/agents/researcher/prompts/old.md"], warnings: [] };
350 +await store.applyPendingMutation();
351 +if (confirmations.length !== 1 || confirmations[0].type !== "danger") throw new Error("danger confirmation missing");
352 +if (!confirmations[0].message.includes("agent.yaml") || !confirmations[0].message.includes("old.md")) throw new Error("planned paths missing from confirmation");
353 +if (confirmations[0].title !== "Delete all customizations for this profile?") throw new Error("cleanup confirmation title mismatch");
354 """
355 module_source = harness + "\n" + source + "\n" + checks
356 module_url = "data:text/javascript;base64," + base64.b64encode(