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";