Polish Agent Editor profile management

Add scoped restore for inherited profiles through the existing sparse removal path, and streamline Manage agents with separator rows and no redundant footer. Remove redundant customization signals, use theme-primary colors for compact identity actions, simplify delegation guidance, and extend focused WebUI regressions.

Alessandro committed Aug 10, 2026 at 05:51 UTC 7b9f06771f4af57e7ed2a96b7ebe97961c677738
4 files changed +65 -19
plugins/_agent_editor/AGENTS.md
+4 -3
@@ -55,9 +55,10 @@
55 editor; Agent Editor persists only the scoped preset reference.
56 - Manage agents reuses the plugin-settings project vocabulary: Global or one
57 existing project. The active chat profile appears once above the list; each
58 - row exposes scoped availability, duplication, icon-only Edit, and Delete for
59 - profiles owned by that scope. Duplicate materializes the effective source
60 - profile into the selected writable layer with a collision-free ID and title.
58 + row exposes scoped availability, duplication, restore for inherited profiles,
59 + icon-only Edit, and Delete for profiles owned by that scope. Duplicate
60 + materializes the effective source profile into the selected writable layer
61 + with a collision-free ID and title.
62 Availability changes quietly refresh the adjacent profile switcher catalog
63 without a success toast.
64 - The same project selector is available inside Create and Edit. Create keeps
plugins/_agent_editor/webui/agent-editor-store.js
+27
@@ -297,6 +297,33 @@ const model = {
297 }
298 },
299
300 + async restoreProfile(profile) {
301 + if (!profile?.id || profile.deletable || !profile.scope_has_overrides || this.saving) return;
302 + const confirmed = await showConfirmDialog({
303 + title: `Restore ${escapeHtml(profile.title || profile.id)}?`,
304 + message: `<p>This removes your Agent Editor customizations from ${escapeHtml(this.scopeLabel)}. Other files stay in place.</p>`,
305 + confirmText: "Restore original",
306 + type: "danger",
307 + });
308 + if (!confirmed) return;
309 + this.saving = true;
310 + this.error = "";
311 + try {
312 + await callJsonApi(API, {
313 + action: "remove_changes",
314 + profile_id: profile.id,
315 + destructive: false,
316 + ...this.scopeInput(),
317 + });
318 + await this.loadProfiles();
319 + await modelConfigStore.loadAgentProfiles(true);
320 + } catch (error) {
321 + this.error = error.message || String(error);
322 + } finally {
323 + this.saving = false;
324 + }
325 + },
326 +
327 async onScopeChanged() {
328 const previous = this.intent.projectName || "";
329 const next = this.projectName || "";
plugins/_agent_editor/webui/main.html
+16 -11
@@ -66,7 +66,6 @@
66 <div class="agent-manager-name">
67 <strong x-text="profile.title || profile.id"></strong>
68 <span class="agent-origin" x-text="profile.origin"></span>
69 - <span class="agent-customized-indicator" x-show="profile.scope_has_overrides" title="Customized" aria-label="Customized" role="img"><x-icon name="edit_note"></x-icon></span>
69 </div>
70 <p x-text="profile.description || 'No description'"></p>
71 <button type="button" class="text-button agent-manager-inline-action"
@@ -75,6 +74,13 @@
74 @click="$store.agentEditor.duplicateProfile(profile)">
75 <x-icon name="content_copy"></x-icon><span>Duplicate</span>
76 </button>
77 + <button type="button" class="text-button agent-manager-inline-action"
78 + x-show="profile.scope_has_overrides && !profile.deletable"
79 + :disabled="$store.agentEditor.saving"
80 + :aria-label="`Restore original ${profile.title || profile.id}`"
81 + @click="$store.agentEditor.restoreProfile(profile)">
82 + <x-icon name="restore"></x-icon><span>Restore original</span>
83 + </button>
84 </div>
85 <div class="agent-manager-actions">
86 <label class="toggle agent-profile-availability"
@@ -218,7 +224,7 @@
224 </div>
225 <small>Use the current preset or choose another setup for this agent.</small>
226 </div>
221 - <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-show="$store.agentEditor.metadataProvenance('context')" 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>
227 + <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; leaving this empty may reduce delegation quality.</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></div>
228 </div>
229 </div>
230 </section>
@@ -326,10 +332,9 @@
332 </div>
333 </template>
334
329 - <div class="modal-footer agent-editor-footer" data-modal-footer x-show="!$store.agentEditor.loading && !($store.agentEditor.view === 'editor' && $store.agentEditor.pendingMutation)">
335 + <div class="modal-footer agent-editor-footer" data-modal-footer x-show="!$store.agentEditor.loading && $store.agentEditor.view === 'editor' && !$store.agentEditor.pendingMutation">
336 <div class="footer-left">
331 - <button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'manage'" @click="window.closeModal?.()">Close</button>
332 - <button type="button" class="btn btn-cancel" x-show="$store.agentEditor.view === 'editor'" @click="window.closeModal?.()">Cancel</button>
337 + <button type="button" class="btn btn-cancel" @click="window.closeModal?.()">Cancel</button>
338 </div>
339 <div class="footer-actions">
340 <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 > 0">Save & test</button>
@@ -380,7 +385,7 @@
385 .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); }
386 .agent-avatar img { width:100%; height:100%; object-fit:cover; }
387 .avatar-progress { position:absolute; inset:0; display:grid; place-items:center; background:rgba(0,0,0,.5); }
383 - .agent-avatar-actions { display:flex; align-items:center; justify-content:center; gap:.35rem; font-size:.76rem; white-space:nowrap; }
388 + .agent-avatar-actions { --agent-editor-action:var(--color-primary); display:flex; align-items:center; justify-content:center; gap:.35rem; font-size:.76rem; white-space:nowrap; }
389 .avatar-color-action,.avatar-upload-action { position:relative; cursor:pointer; }
390 .avatar-action-icon { display:grid; place-items:center; width:1.75rem; height:1.75rem; border-radius:6px; color:var(--agent-editor-action); }
391 .avatar-action-icon x-icon { font-size:1.05rem; }
@@ -399,7 +404,7 @@
404 .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; }
405 .agent-editor .text-button:hover { text-decoration:underline; }
406 .agent-easy textarea { min-height:11rem; resize:vertical; }
402 - .restore-action { display:inline-flex; align-items:center; gap:.25rem; margin-top:.4rem; }
407 + .restore-action { --agent-editor-action:var(--color-primary); display:inline-flex; align-items:center; gap:.25rem; margin-top:.4rem; }
408 .easy-tool-list { display:flex; flex-direction:column; padding:.2rem .35rem .2rem 0; }
409 .easy-tool-list .policy-item { cursor:pointer; }
410 .easy-skills-hint { margin:.55rem 0 0; color:var(--color-text-secondary); font-size:.79rem; }
@@ -521,16 +526,16 @@
526 .active-agent-avatar { width:1.5rem; aspect-ratio:1; display:grid; flex:0 0 auto; place-items:center; overflow:hidden; border-radius:6px; color:#fff; font-size:.65rem; font-weight:700; }
527 .active-agent-avatar img { width:100%; height:100%; object-fit:cover; }
528 .agent-manager-create { flex:0 0 auto; display:inline-flex; align-items:center; gap:.35rem; }
524 - .agent-manager-list { display:flex; flex-direction:column; gap:.55rem; }
525 - .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; }
529 + .agent-manager-list { display:flex; flex-direction:column; }
530 + .agent-manager-card { display:grid; grid-template-columns:3rem minmax(0,1fr) auto; gap:.75rem; align-items:center; padding:.65rem .25rem; }
531 + .agent-manager-card + .agent-manager-card { border-top:1px solid var(--color-border); }
532 .agent-manager-avatar { width:3rem; aspect-ratio:1; display:grid; place-items:center; border-radius:10px; color:white; font-weight:700; overflow:hidden; }
533 .agent-manager-avatar img { width:100%; height:100%; object-fit:cover; }
534 .agent-manager-copy { min-width:0; }
535 .agent-manager-name { display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; }
536 .agent-manager-name strong,.agent-manager-copy p { overflow-wrap:anywhere; }
531 - .agent-customized-indicator { display:inline-grid; place-items:center; color:var(--color-text-secondary); }
532 - .agent-customized-indicator x-icon { font-size:1rem; }
537 .agent-editor .agent-manager-inline-action { gap:.25rem; margin-top:.15rem; color:var(--color-message-text); opacity:.7; }
538 + .agent-editor .agent-manager-inline-action + .agent-manager-inline-action { margin-left:.5rem; }
539 .agent-editor .agent-manager-inline-action:hover:not(:disabled) { background:transparent; color:var(--color-text); text-decoration:none; opacity:1; }
540 .agent-manager-actions { display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; }
541 .agent-profile-availability input { width:0; height:0; margin:0; border:0; }
tests/test_agent_editor_webui.py
+18 -5
@@ -103,9 +103,7 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
103 assert '<option value="">Global</option>' in modal
104 assert 'x-for="project in $store.agentEditor.projects"' in modal
105 assert 'x-show="profile.deletable"' in modal
106 - assert 'x-show="profile.scope_has_overrides"' in modal
107 - assert 'class="agent-customized-indicator"' in modal
108 - assert 'title="Customized"' in modal
106 + assert 'class="agent-customized-indicator"' not in modal
107 assert 'class="button agent-manager-create"' in modal
108 assert 'class="active-agent-display"' in modal
109 assert 'class="button icon-button" title="Edit"' in modal
@@ -118,6 +116,9 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
116 assert ':disabled="!!$store.agentEditor.duplicatingProfile"' in modal
117 assert ':aria-label="`Duplicate ${profile.title || profile.id}`"' in modal
118 assert "$store.agentEditor.duplicateProfile(profile)" in modal
119 + assert 'x-show="profile.scope_has_overrides && !profile.deletable"' in modal
120 + assert ':aria-label="`Restore original ${profile.title || profile.id}`"' in modal
121 + assert "$store.agentEditor.restoreProfile(profile)" in modal
122 assert 'class="toggle agent-profile-availability"' in modal
123 assert ':disabled="$store.agentEditor.profileAvailabilitySaving"' in modal
124 assert "Default is always available" not in modal
@@ -158,7 +159,7 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
159 assert modal.count('role="alert"') >= 4
160 assert "Fix ${$store.agentEditor.validationIssues().length}" in modal
161 assert modal.count("$store.agentEditor.validationIssues().length > 0") == 2
161 - assert "!($store.agentEditor.view === 'editor' && $store.agentEditor.pendingMutation)" in modal
162 + assert "$store.agentEditor.view === 'editor' && !$store.agentEditor.pendingMutation" in modal
163 assert 'x-show="$store.agentEditor.view === \'editor\' && !$store.agentEditor.draft?.creating"' in modal
164 assert 'class="btn btn-ok" x-show="$store.agentEditor.view === \'editor\'"' in modal
165 assert "Delete all customizations in" in modal
@@ -166,8 +167,9 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
167 assert 'promptDisplayState(prompt)' in modal
168 assert 'promptSourceChain($store.agentEditor.selectedPromptDraft)' in modal
169 assert "Will reset to inherited on save." in modal
169 - for key in ("title", "description", "context"):
170 + for key in ("title", "description"):
171 assert f'x-show="$store.agentEditor.metadataProvenance(\'{key}\')"' in modal
172 + assert 'metadataProvenance(\'context\')' not in modal
173 assert ':title="prompt.filename"' not in modal
174 assert "promptEditPending($store.agentEditor.selectedPromptDraft)" in modal
175 assert 'aria-label="Discard current edit"' in modal
@@ -210,6 +212,7 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
212 assert "@keydown.ctrl.s.prevent" in modal
213 assert "@media (max-width: 760px)" in modal
214 assert modal.count("data-modal-footer") == 1
215 + assert ">Close</button>" not in modal
216
217
218 def test_agent_editor_store_has_no_conversational_or_model_builder_path() -> None:
@@ -397,6 +400,16 @@ if (toggleRequest !== 2 || rapidProfiles[1].enabled || store.profileAvailability
400 setEnabledHandler = null;
401 await store.duplicateProfile({ id: "researcher", title: "Researcher" });
402 if (!calls.some(item => item.payload?.action === "duplicate" && item.payload.profile_id === "researcher") || !store.profiles.some(profile => profile.id === "researcher-1")) throw new Error("profile duplication failed");
403 +calls.length = 0;
404 +confirmResult = false;
405 +await store.restoreProfile({ id: "researcher", title: "Researcher", scope_has_overrides: true, deletable: false });
406 +if (calls.length || confirmations.at(-1)?.title !== "Restore Researcher?") throw new Error("restore cancellation failed");
407 +confirmResult = true;
408 +await store.restoreProfile({ id: "researcher", title: "Researcher", scope_has_overrides: true, deletable: false });
409 +if (!calls.some(item => item.payload?.action === "remove_changes" && item.payload.profile_id === "researcher" && item.payload.destructive === false)) throw new Error("restore original did not use sparse removal");
410 +if (!calls.some(item => item.endpoint === "loadAgentProfiles" && item.payload === true) || store.saving) throw new Error("restore original did not refresh profile state");
411 +confirmations.length = 0;
412 +confirmResult = false;
413 store.projectName = "other";
414 if (store.currentChatUsesScope() || store.isProfileActive("default")) throw new Error("foreign project profile appeared active");
415 store.projectName = "demo";