Unify Agent Editor capability and prompt controls
Give Tools, MCPs, and Skills explicit default switches with segmented per-item policy controls while preserving sparse overrides. Simplify Advanced prompt editing around the full-height ACE surface and align the editor contracts and regression coverage.
Alessandro committed
Aug 11, 2026 at 14:22 UTC
13ecab24ee8bc8ceacea9cc139652b5d1c4481af
12 files changed
+618
-394
helpers/tool_policy.py
+6
-2
@@ -28,8 +28,10 @@ def normalize_policy(config: Any) -> dict[str, Any]:
28
raw = dict(config) if isinstance(config, dict) else {}
29
mode = str(raw.get("mode") or "inherit").strip().lower()
30
default = str(raw.get("default") or "allow").strip().lower()
31
+ mcp_default = str(raw.get("mcp_default") or "allow").strip().lower()
32
raw["mode"] = "custom" if mode == "custom" else "inherit"
33
raw["default"] = "block" if default == "block" else "allow"
34
+ raw["mcp_default"] = "block" if mcp_default == "block" else "allow"
35
raw["allowed"] = _normalize_ids(raw.get("allowed"))
36
raw["blocked"] = _normalize_ids(raw.get("blocked"))
37
return raw
@@ -49,7 +51,8 @@ def get_policy(agent: Any) -> dict[str, Any]:
51
):
52
config = files.read_file_json(asset["path"])
53
if not isinstance(config, dict) or not any(
52
- key in config for key in ("mode", "default", "allowed", "blocked")
54
+ key in config
55
+ for key in ("mode", "default", "mcp_default", "allowed", "blocked")
56
):
57
continue
58
policy = normalize_policy(config)
@@ -171,7 +174,8 @@ def resolve_tool(
174
if tool_id in policy["allowed"]:
175
return ToolPolicyDecision(True, tool_id, "scoped-policy", "custom")
176
174
- is_allowed = policy["default"] == "allow"
177
+ default_key = "mcp_default" if tool_id.startswith("mcp:") else "default"
178
+ is_allowed = policy[default_key] == "allow"
179
return ToolPolicyDecision(
180
is_allowed,
181
tool_id,
helpers/tool_policy.py.dox.md
+3
-2
@@ -31,8 +31,9 @@
31
active project, user profile, bundled/plugin profile, then default.
32
`get_policy` selects the first custom policy; unknown-only and
33
explicit-inherit files remain on disk but defer to the next lower layer.
34
-- Missing policy inherits standard access; custom policy always records whether
35
- future tools default to allowed or blocked.
34
+- Missing policy inherits standard access. A custom policy records independent
35
+ defaults for local/plugin tools and canonical MCP tools; explicit allowed or
36
+ blocked IDs take precedence over either default.
37
- The `response` capability is a framework-required invariant: profile policy
38
cannot disable it, and the editor does not list it as a configurable tool.
39
- `vision_load` remains owned by the active chat model's vision configuration;
plugins/_agent_editor/AGENTS.md
+17
-10
@@ -44,18 +44,25 @@
44
Removing or deleting in project scope never mutates those inherited layers;
45
only agents created in the selected scope are deletable.
46
- Bundled `agents/` files are read-only.
47
-- Advanced prompt text is directly editable; per-file close/check actions
48
- discard or accept the current edit checkpoint, while the editor's global save
49
- remains the only persistence boundary.
47
+- Advanced prompt text is edited with a full-height bundled ACE editor in
48
+ Markdown mode. The selected file's customization path sits below its name;
49
+ per-file close/check actions discard or accept the current edit checkpoint,
50
+ while the editor's global save remains the only persistence boundary.
51
- New profiles require a display name and non-empty agent instructions in both
52
Easy and Advanced; existing Advanced prompt edits retain per-file semantics.
52
-- The configurable tool catalog is visible in both modes; Easy provides direct
53
- allow/block checkboxes and points to Advanced for skill access. Skills remain
54
- Advanced-only. Advanced keeps both complete selectors visible but disabled
55
- for inherited access and interactive for custom access. Framework-required
56
- tools remain absent from the tool catalog.
57
-- Model selection reuses `_model_config`'s compact preset dropdown and preset
58
- editor; Agent Editor persists only the scoped preset reference.
53
+- Easy and Advanced share the same segmented capability controls: Default
54
+ removes the item from `allowed` and `blocked`, On stores it in `allowed`, and
55
+ Off stores it in `blocked`. Tools, canonical MCP entries, and Skills expose
56
+ independent default switches; explicit choices remain pinned when a default
57
+ changes, and opening then undoing an inherited policy produces no write. Easy
58
+ places each initially closed native accordion directly below its default
59
+ switch. Advanced gives Tools, MCPs, and Skills separate sections while keeping
60
+ unavailable retained IDs reviewable. Framework-required tools remain absent.
61
+- Model selection in both modes reuses `_model_config`'s compact preset dropdown
62
+ and preset editor; Agent Editor persists only the scoped preset reference.
63
+- The exact `default` profile is an internal baseline and is omitted only from
64
+ selectable and editable UI rows. Runtime discovery remains unchanged, and an
65
+ existing chat using it may still report it as current status.
66
- Manage agents reuses the plugin-settings project vocabulary: Global or one
67
existing project. The active chat profile appears once above the list; each
68
row exposes scoped availability, duplication, restore for inherited profiles,
plugins/_agent_editor/README.md
+8
@@ -8,6 +8,12 @@ The editor never invokes a model. Tool and skill controls are backed by the
8
central runtime policy owners, and every save is previewed as exact file writes
9
and deletions before the same validated plan is applied.
10
11
+Easy and Advanced use the same segmented capability policy: On pins an item
12
+allowed, Off pins it blocked, and Default follows the profile's Tools, MCPs, or
13
+Skills default switch. Easy places each chooser below its default switch;
14
+Advanced gives Tools, MCPs, and Skills separate searchable sections and adds
15
+retained unavailable entries plus ACE-based prompt editing.
16
+
17
Global agents and customizations live under `usr/agents/<profile-id>` and apply
18
across projects. Project-scoped agents and customizations live under
19
`usr/projects/<project>/.a0proj/agents/<profile-id>`, inherit the Global layer,
@@ -17,3 +23,5 @@ Manage agents can duplicate the effective profile into the selected scope and
23
toggle whether each profile is available there. Project availability reuses
24
`.a0proj/agents.json`; Global availability is a sparse profile override.
25
The selected scope must always keep at least one profile available.
26
+The bundled `default` profile remains the internal inheritance baseline and is
27
+not offered as a selectable or editable profile.
plugins/_agent_editor/helpers/editor.py
+11
-5
@@ -32,7 +32,7 @@ RESERVED_PROFILE_IDS = {"_example"}
32
NON_PROMPT_MARKDOWN = {"AGENTS.md"}
33
USER_AGENTS_ROOT = Path(files.get_abs_path(subagents.USER_AGENTS_DIR))
34
STAGED_AVATAR_ROOT = Path(files.get_abs_path("tmp", "agent-editor"))
35
-_POLICY_KEYS = ("mode", "default", "allowed", "blocked")
35
+_TOOL_POLICY_KEYS = ("mode", "default", "mcp_default", "allowed", "blocked")
36
_MUTATION_LOCK = threading.RLock()
37
38
@@ -319,7 +319,7 @@ def build_editor_state(
319
"tools": {
320
"policy": tool_policy.normalize_policy(tool_scope),
321
"effective_policy": tool_policy.get_policy(agent),
322
- "has_override": any(key in tool_scope for key in _POLICY_KEYS),
322
+ "has_override": any(key in tool_scope for key in _TOOL_POLICY_KEYS),
323
"catalog": tool_catalog,
324
},
325
"skills": {
@@ -1020,11 +1020,17 @@ def _plan_tool_policy(plan: ChangePlan, value: Any) -> None:
1020
data = _read_mapping_strict(path, "tool policy configuration")
1021
1022
if mode == "inherit":
1023
- for key in _POLICY_KEYS:
1023
+ for key in _TOOL_POLICY_KEYS:
1024
data.pop(key, None)
1025
else:
1026
if mode == "off":
1027
- policy = {"mode": "custom", "default": "block", "allowed": [], "blocked": []}
1027
+ policy = {
1028
+ "mode": "custom",
1029
+ "default": "block",
1030
+ "mcp_default": "block",
1031
+ "allowed": [],
1032
+ "blocked": [],
1033
+ }
1034
elif mode == "custom":
1035
policy = tool_policy.normalize_policy(section)
1036
allowed = set(policy["allowed"])
@@ -1036,7 +1042,7 @@ def _plan_tool_policy(plan: ChangePlan, value: Any) -> None:
1042
raise ValueError(f'Invalid canonical tool ID "{tool_id}".')
1043
else:
1044
raise ValueError("Tool policy mode must be inherit, off, or custom.")
1039
- data.update({key: policy[key] for key in _POLICY_KEYS})
1045
+ data.update({key: policy[key] for key in _TOOL_POLICY_KEYS})
1046
_plan_json_mapping(plan, path, data)
1047
1048
plugins/_agent_editor/webui/agent-editor-store.js
+184
-158
@@ -29,7 +29,7 @@ export function slugifyProfileName(value) {
29
.replace(/[-_]+$/g, "");
30
}
31
32
-function policyFromState(value, hasOverride) {
32
+function policyFromState(value, hasOverride, includeMcpDefault = false) {
33
const policy = value && typeof value === "object" ? value : {};
34
const normalized = {
35
mode: policy.mode === "custom" ? "custom" : "inherit",
@@ -37,6 +37,9 @@ function policyFromState(value, hasOverride) {
37
allowed: unique(policy.allowed),
38
blocked: unique(policy.blocked),
39
};
40
+ if (includeMcpDefault) {
41
+ normalized.mcp_default = policy.mcp_default === "block" ? "block" : "allow";
42
+ }
43
if (!hasOverride || normalized.mode !== "custom") normalized.mode = "inherit";
44
return normalized;
45
}
@@ -45,27 +48,42 @@ function policyAllows(policy, id) {
48
if (!policy || policy.mode !== "custom") return true;
49
if (policy.blocked.includes(id)) return false;
50
if (policy.allowed.includes(id)) return true;
48
- return policy.default === "allow";
51
+ const fallback = String(id || "").startsWith("mcp:") ? policy.mcp_default : policy.default;
52
+ return fallback === "allow";
53
+}
54
+
55
+function policyItemState(policy, id) {
56
+ if (!policy || policy.mode !== "custom") return "default";
57
+ if (policy?.blocked?.includes(id)) return "block";
58
+ if (policy?.allowed?.includes(id)) return "allow";
59
+ return "default";
60
}
61
51
-function policyBehavior(policy) {
62
+function policyBehavior(policy, includeMcpDefault = false) {
63
const value = policy || {};
64
if (value.mode !== "custom") {
54
- return { default: "allow", allowed: [], blocked: [] };
65
+ return {
66
+ default: "allow",
67
+ ...(includeMcpDefault ? { mcp_default: "allow" } : {}),
68
+ allowed: [],
69
+ blocked: [],
70
+ };
71
}
72
return {
73
default: value.default === "block" ? "block" : "allow",
74
+ ...(includeMcpDefault
75
+ ? { mcp_default: value.mcp_default === "block" ? "block" : "allow" }
76
+ : {}),
77
allowed: unique(value.allowed).sort(),
78
blocked: unique(value.blocked).sort(),
79
};
80
}
81
63
-function movePolicyItem(policy, id, allow) {
82
+function setPolicyItemState(policy, id, state) {
83
policy.allowed = policy.allowed.filter((item) => item !== id);
84
policy.blocked = policy.blocked.filter((item) => item !== id);
66
- const exceptions = allow ? "allowed" : "blocked";
67
- const matchesDefault = allow === (policy.default === "allow");
68
- if (!matchesDefault) policy[exceptions].push(id);
85
+ if (state === "allow") policy.allowed.push(id);
86
+ if (state === "block") policy.blocked.push(id);
87
}
88
89
function escapeHtml(value) {
@@ -93,17 +111,14 @@ const model = {
111
initialDraft: null,
112
selectedPrompt: SPECIFICS,
113
promptFileSearch: "",
96
- promptTextSearch: "",
97
- comparePrompt: "",
114
toolSearch: "",
99
- toolCategory: "all",
115
toolOrigin: "all",
101
- selectedAllowedTools: [],
102
- selectedBlockedTools: [],
116
skillSearch: "",
117
skillOrigin: "all",
105
- selectedAllowedSkills: [],
106
- selectedBlockedSkills: [],
118
+ promptEditor: null,
119
+ promptEditorChangeHandler: null,
120
+ settingPromptEditorValue: false,
121
+ aceUnavailable: false,
122
promptEditBaselines: {},
123
plan: { written: [], deleted: [], warnings: [] },
124
planLoading: false,
@@ -136,10 +151,15 @@ const model = {
151
: String(options.projectName || ""),
152
};
153
this.suppressClosePrompt = false;
139
- return await openModal(MODAL, () => this.beforeClose());
154
+ try {
155
+ return await openModal(MODAL, () => this.beforeClose());
156
+ } finally {
157
+ this.destroyPromptEditor();
158
+ }
159
},
160
161
async mount(root) {
162
+ this.destroyPromptEditor();
163
this.revokePreview();
164
this.root = root;
165
this.draft = null;
@@ -248,6 +268,10 @@ const model = {
268
return this.profiles.find((profile) => this.isProfileActive(profile.id)) || null;
269
},
270
271
+ get visibleProfiles() {
272
+ return this.profiles.filter((profile) => profile.id !== "default");
273
+ },
274
+
275
async setProfileEnabled(profile, enabled) {
276
if (!profile?.id || this.profileAvailabilitySaving) return;
277
const previous = !!profile.enabled;
@@ -343,12 +367,11 @@ const model = {
367
this.projectName = previous;
368
return;
369
}
346
- const reviewActive = this.mode === "advanced" && this.section === "5";
370
+ const reviewActive = this.mode === "advanced" && this.section === "6";
371
const creatingDraft = this.draft?.creating ? this.draft : null;
372
const creatingInitial = creatingDraft ? this.initialDraft : null;
373
const creatingState = creatingDraft ? this.state : null;
374
const selectedPrompt = this.selectedPrompt;
351
- const comparePrompt = this.comparePrompt;
375
const promptChanges = creatingDraft
376
? Object.values(creatingDraft.prompts).flatMap((prompt) => {
377
const initial = creatingInitial?.prompts?.[prompt.filename];
@@ -376,13 +399,14 @@ const model = {
399
key,
400
idKey,
401
default: policy.default,
402
+ mcpDefault: policy.mcp_default,
403
choices: (oldState.catalog || [])
404
.map((item) => [
405
item[idKey],
382
- policyAllows(policy, item[idKey]),
383
- policyAllows(oldState.effective_policy, item[idKey]),
406
+ policyItemState(policy, item[idKey]),
407
+ policyItemState(oldState.effective_policy, item[idKey]),
408
])
385
- .filter(([, allowed, inherited]) => allowed !== inherited),
409
+ .filter(([, state, inherited]) => state !== inherited),
410
});
411
}
412
}
@@ -426,7 +450,6 @@ const model = {
450
if (change.baseline) this.promptEditBaselines[change.filename] = change.baseline;
451
}
452
if (this.draft.prompts[selectedPrompt]) this.selectedPrompt = selectedPrompt;
429
- this.comparePrompt = comparePrompt;
453
if (avatarChanged) {
454
this.draft.avatar = clone(creatingDraft.avatar);
455
this.draft.avatarToken = creatingDraft.avatarToken;
@@ -438,12 +461,16 @@ const model = {
461
if (modelPresetChanged) this.draft.modelPreset = creatingDraft.modelPreset;
462
for (const change of policyChanges) {
463
this.customizePolicy(change.kind);
441
- this.setPolicyDefault(change.kind, change.default);
464
+ this.draft[change.key].default = change.default;
465
+ if (change.kind === "tool") {
466
+ this.draft[change.key].mcp_default = change.mcpDefault;
467
+ }
468
const catalog = this.state[change.kind === "tool" ? "tools" : "skills"].catalog || [];
469
const nextIds = new Set(catalog.map((item) => item[change.idKey]));
444
- for (const [id, allowed] of change.choices) {
445
- if (nextIds.has(id)) movePolicyItem(this.draft[change.key], id, allowed);
470
+ for (const [id, state] of change.choices) {
471
+ if (nextIds.has(id)) setPolicyItemState(this.draft[change.key], id, state);
472
}
473
+ this.collapsePolicy(change.kind);
474
}
475
}
476
if (reviewActive) await this.previewPlan();
@@ -524,23 +551,19 @@ const model = {
551
modelPreset: this.state.model_preset.has_override
552
? String(this.state.model_preset.override || "")
553
: "",
527
- toolPolicy: policyFromState(this.state.tools.policy, this.state.tools.has_override),
554
+ toolPolicy: policyFromState(this.state.tools.policy, this.state.tools.has_override, true),
555
skillPolicy: policyFromState(this.state.skills.policy, this.state.skills.has_override),
556
};
557
this.initialDraft = clone(this.draft);
558
this.selectedPrompt = SPECIFICS;
532
- this.comparePrompt = "";
559
this.planStatus = "idle";
534
- this.selectedAllowedTools = [];
535
- this.selectedBlockedTools = [];
536
- this.selectedAllowedSkills = [];
537
- this.selectedBlockedSkills = [];
560
this.promptEditBaselines = Object.fromEntries(
561
Object.values(prompts).map((prompt) => [prompt.filename, {
562
value: prompt.value,
563
reset: prompt.reset,
564
}]),
565
);
566
+ this.schedulePromptEditor();
567
},
568
569
get dirty() {
@@ -564,6 +587,14 @@ const model = {
587
return (this.state?.tools?.catalog || []).filter((item) => item.available !== false);
588
},
589
590
+ get standardToolCatalog() {
591
+ return this.toolCatalog.filter((item) => !String(item.id || "").startsWith("mcp:"));
592
+ },
593
+
594
+ get mcpCatalog() {
595
+ return this.toolCatalog.filter((item) => String(item.id || "").startsWith("mcp:"));
596
+ },
597
+
598
get toolOrigins() {
599
return unique((this.state?.tools?.catalog || []).map((item) => item.origin)).sort();
600
},
@@ -599,8 +630,8 @@ const model = {
630
);
631
}
632
if (String(section) === "2") return Object.values(this.draft.prompts).some((prompt) => this.promptDirty(prompt));
602
- if (String(section) === "3") return !same(this.draft.toolPolicy, this.initialDraft.toolPolicy);
603
- if (String(section) === "4") return !same(this.draft.skillPolicy, this.initialDraft.skillPolicy);
633
+ if (["3", "4"].includes(String(section))) return !same(this.draft.toolPolicy, this.initialDraft.toolPolicy);
634
+ if (String(section) === "5") return !same(this.draft.skillPolicy, this.initialDraft.skillPolicy);
635
return this.dirty;
636
},
637
@@ -622,6 +653,7 @@ const model = {
653
},
654
655
enterManager() {
656
+ this.destroyPromptEditor();
657
this.revokePreview();
658
this.draft = null;
659
this.initialDraft = null;
@@ -638,12 +670,13 @@ const model = {
670
setMode(mode, section = "", preview = true) {
671
this.mode = mode === "advanced" ? "advanced" : "easy";
672
if (section) this.setSection(section, preview);
641
- else if (preview && this.mode === "advanced" && this.section === "5") this.previewPlan();
673
+ else if (preview && this.mode === "advanced" && this.section === "6") this.previewPlan();
674
this.syncSurface();
675
if (this.mode === "advanced") {
676
requestAnimationFrame(() => {
677
this.root?.querySelector(`[data-agent-editor-section="${this.section}"]`)?.focus();
678
});
679
+ this.schedulePromptEditor();
680
}
681
},
682
@@ -652,7 +685,8 @@ const model = {
685
try {
686
localStorage.setItem(LAST_SECTION_KEY, this.section);
687
} catch {}
655
- if (preview && this.section === "5") this.previewPlan();
688
+ if (preview && this.section === "6") this.previewPlan();
689
+ if (this.section === "2") this.schedulePromptEditor();
690
},
691
692
savedSection() {
@@ -791,6 +825,7 @@ const model = {
825
prompt.value = String(prompt.inherited || "");
826
prompt.reset = true;
827
this.acceptPromptEdit(prompt.filename);
828
+ this.syncPromptEditor();
829
},
830
831
markPromptSet(filename) {
@@ -830,6 +865,7 @@ const model = {
865
if (!prompt || !baseline) return;
866
prompt.value = baseline.value;
867
prompt.reset = baseline.reset;
868
+ this.syncPromptEditor();
869
},
870
871
resetPrompt(filename) {
@@ -838,6 +874,7 @@ const model = {
874
prompt.value = prompt.inherited;
875
prompt.reset = true;
876
this.acceptPromptEdit(filename);
877
+ this.syncPromptEditor();
878
},
879
880
promptDisplayState(prompt) {
@@ -847,25 +884,10 @@ const model = {
884
return this.projectName ? "Inherited" : "Default";
885
},
886
850
- promptSourceChain(prompt) {
851
- if (!prompt?.reset && (prompt?.has_override || this.promptDirty(prompt))) return "Customized by you";
852
- if (this.projectName) return "Inherited from Global";
853
- const source = [...(prompt?.source_chain || [])]
854
- .filter((item) => item !== "Your override")
855
- .at(-1);
856
- const current = String(
857
- this.state?.profile?.metadata?.title?.effective || this.state?.profile?.id || "",
858
- );
859
- return !source || source.toLowerCase() === current.toLowerCase()
860
- ? "Default"
861
- : `Inherited from ${source}`;
862
- },
863
-
887
selectPrompt(filename) {
888
if (!this.draft?.prompts?.[filename]) return;
889
this.selectedPrompt = filename;
867
- this.promptTextSearch = "";
868
- this.comparePrompt = "";
890
+ this.schedulePromptEditor();
891
},
892
893
filteredPromptFiles(group = "") {
@@ -880,26 +902,68 @@ const model = {
902
return Boolean(prompt?.reset || prompt?.value !== prompt?.initialValue);
903
},
904
883
- promptMatchCount() {
884
- const query = this.promptTextSearch;
885
- const text = this.selectedPromptDraft?.value || "";
886
- if (!query) return 0;
887
- return text.toLowerCase().split(query.toLowerCase()).length - 1;
905
+ schedulePromptEditor() {
906
+ if (this.mode !== "advanced" || this.section !== "2") return;
907
+ requestAnimationFrame(() => requestAnimationFrame(() => this.initPromptEditor()));
908
+ },
909
+
910
+ initPromptEditor() {
911
+ const container = this.root?.querySelector("#agent-editor-prompt-ace");
912
+ if (!container) return;
913
+ if (this.promptEditor && !this.root?.contains?.(this.promptEditor.container)) {
914
+ this.destroyPromptEditor();
915
+ }
916
+ if (this.promptEditor) {
917
+ this.syncPromptEditor();
918
+ this.promptEditor.resize?.(true);
919
+ return;
920
+ }
921
+ if (!globalThis.ace?.edit) {
922
+ this.aceUnavailable = true;
923
+ return;
924
+ }
925
+ const editor = globalThis.ace.edit(container);
926
+ const darkMode = globalThis.localStorage?.getItem("darkMode");
927
+ editor.setTheme(darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/github");
928
+ editor.session.setMode("ace/mode/markdown");
929
+ editor.session.setUseWrapMode(true);
930
+ editor.setOptions({ showPrintMargin: false, useWorker: false });
931
+ editor.setValue(this.selectedPromptDraft?.value || "", -1);
932
+ this.promptEditorChangeHandler = () => {
933
+ if (this.settingPromptEditorValue || !this.selectedPromptDraft) return;
934
+ this.selectedPromptDraft.value = editor.getValue();
935
+ this.onPromptInput(this.selectedPrompt);
936
+ };
937
+ editor.session.on("change", this.promptEditorChangeHandler);
938
+ editor.textInput?.getElement?.()?.setAttribute("aria-label", "Prompt Markdown");
939
+ this.promptEditor = editor;
940
+ this.aceUnavailable = false;
941
+ },
942
+
943
+ syncPromptEditor() {
944
+ if (!this.promptEditor) {
945
+ this.schedulePromptEditor();
946
+ return;
947
+ }
948
+ const value = String(this.selectedPromptDraft?.value || "");
949
+ if (this.promptEditor.getValue() !== value) {
950
+ this.settingPromptEditorValue = true;
951
+ this.promptEditor.setValue(value, -1);
952
+ this.settingPromptEditorValue = false;
953
+ }
954
+ this.promptEditor.resize?.(true);
955
},
956
890
- findInPrompt(direction = 1) {
891
- const textarea = this.root?.querySelector("#agent-editor-prompt-text");
892
- const query = this.promptTextSearch;
893
- const text = this.selectedPromptDraft?.value || "";
894
- if (!textarea || !query) return;
895
- const lower = text.toLowerCase();
896
- const needle = query.toLowerCase();
897
- const start = direction > 0 ? textarea.selectionEnd : Math.max(0, textarea.selectionStart - 1);
898
- let index = direction > 0 ? lower.indexOf(needle, start) : lower.lastIndexOf(needle, start);
899
- if (index < 0) index = direction > 0 ? lower.indexOf(needle) : lower.lastIndexOf(needle);
900
- if (index < 0) return;
901
- textarea.focus();
902
- textarea.setSelectionRange(index, index + query.length);
957
+ destroyPromptEditor() {
958
+ if (this.promptEditor?.session && this.promptEditorChangeHandler) {
959
+ this.promptEditor.session.off?.("change", this.promptEditorChangeHandler);
960
+ }
961
+ const container = this.promptEditor?.container;
962
+ this.promptEditor?.destroy?.();
963
+ if (container) container.textContent = "";
964
+ this.promptEditor = null;
965
+ this.promptEditorChangeHandler = null;
966
+ this.settingPromptEditorValue = false;
967
},
968
969
promptCustomizationPath() {
@@ -914,55 +978,61 @@ const model = {
978
globalThis.justToast?.("Path copied", "success", 1200, "agent-editor-copy");
979
},
980
917
- useStandardTools() {
918
- this.draft.toolPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
919
- this.selectedAllowedTools = [];
920
- this.selectedBlockedTools = [];
921
- },
922
-
981
customizePolicy(kind) {
924
- const key = kind === "tool" ? "toolPolicy" : "skillPolicy";
982
+ const isSkill = kind === "skill";
983
+ const key = isSkill ? "skillPolicy" : "toolPolicy";
984
if (this.draft[key].mode === "custom") return;
926
- const state = kind === "tool" ? this.state.tools : this.state.skills;
927
- this.draft[key] = { mode: "custom", ...policyBehavior(state.effective_policy) };
985
+ const state = isSkill ? this.state.skills : this.state.tools;
986
+ this.draft[key] = {
987
+ mode: "custom",
988
+ ...policyBehavior(state.effective_policy, !isSkill),
989
+ };
990
},
991
930
- chooseTools() {
931
- this.customizePolicy("tool");
932
- this.setMode("advanced", "3");
992
+ activePolicy(kind) {
993
+ const isSkill = kind === "skill";
994
+ const key = isSkill ? "skillPolicy" : "toolPolicy";
995
+ const state = isSkill ? this.state?.skills : this.state?.tools;
996
+ return this.draft?.[key]?.mode === "custom" ? this.draft[key] : state?.effective_policy;
997
},
998
935
- setEasyToolAllowed(id, allow) {
936
- this.customizePolicy("tool");
937
- this.moveTools([id], allow);
938
- const policy = this.draft.toolPolicy;
939
- if (this.initialDraft?.toolPolicy.mode !== "custom"
940
- && same(policyBehavior(policy), policyBehavior(this.state.tools.effective_policy))) {
941
- this.useStandardTools();
942
- }
999
+ policyItemState(kind, id) {
1000
+ return policyItemState(this.activePolicy(kind), id);
1001
},
1002
945
- useStandardSkills() {
946
- this.draft.skillPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
947
- this.selectedAllowedSkills = [];
948
- this.selectedBlockedSkills = [];
1003
+ setPolicyItem(kind, id, state) {
1004
+ this.customizePolicy(kind);
1005
+ const key = kind === "skill" ? "skillPolicy" : "toolPolicy";
1006
+ setPolicyItemState(this.draft[key], id, state);
1007
+ this.collapsePolicy(kind);
1008
},
1009
951
- chooseSkills() {
952
- this.customizePolicy("skill");
1010
+ collapsePolicy(kind) {
1011
+ const isSkill = kind === "skill";
1012
+ const key = isSkill ? "skillPolicy" : "toolPolicy";
1013
+ const state = isSkill ? this.state.skills : this.state.tools;
1014
+ if (this.initialDraft?.[key]?.mode !== "custom"
1015
+ && same(
1016
+ policyBehavior(this.draft[key], !isSkill),
1017
+ policyBehavior(state.effective_policy, !isSkill),
1018
+ )) {
1019
+ this.draft[key] = clone(this.initialDraft[key]);
1020
+ }
1021
},
1022
1023
setPolicyDefault(kind, nextDefault) {
956
- const policy = kind === "tool" ? this.draft.toolPolicy : this.draft.skillPolicy;
957
- const catalog = kind === "tool" ? this.state.tools.catalog : this.state.skills.catalog;
958
- const ids = catalog.map((item) =>
959
- kind === "tool" ? item.id : item.name,
960
- );
961
- const current = new Map(ids.map((id) => [id, policyAllows(policy, id)]));
962
- policy.default = nextDefault === "block" ? "block" : "allow";
963
- policy.allowed = [];
964
- policy.blocked = [];
965
- for (const [id, allowed] of current) movePolicyItem(policy, id, allowed);
1024
+ this.customizePolicy(kind);
1025
+ const key = kind === "skill" ? "skillPolicy" : "toolPolicy";
1026
+ const policy = this.draft[key];
1027
+ const field = kind === "mcp" ? "mcp_default" : "default";
1028
+ policy[field] = nextDefault === "block" ? "block" : "allow";
1029
+ this.collapsePolicy(kind);
1030
+ },
1031
+
1032
+ policyDefault(kind) {
1033
+ const policy = this.activePolicy(kind);
1034
+ const field = kind === "mcp" ? "mcp_default" : "default";
1035
+ return policy?.mode === "custom" && policy[field] === "block" ? "block" : "allow";
1036
},
1037
1038
isToolAllowed(item) {
@@ -979,73 +1049,26 @@ const model = {
1049
return policyAllows(policy, item.name);
1050
},
1051
982
- filteredTools(allowed) {
1052
+ filteredTools(group = "tool") {
1053
const query = this.toolSearch.trim().toLowerCase();
1054
return (this.state?.tools?.catalog || []).filter((item) => {
985
- if (this.draft.toolPolicy.mode !== "custom" && item.available === false) return false;
986
- if (this.isToolAllowed(item) !== allowed) return false;
987
- const category = item.id.split(":", 1)[0];
988
- if (this.toolCategory !== "all" && category !== this.toolCategory) return false;
1055
+ const isMcp = String(item.id || "").startsWith("mcp:");
1056
+ if ((group === "mcp") !== isMcp) return false;
1057
if (this.toolOrigin !== "all" && item.origin !== this.toolOrigin) return false;
1058
return !query || [item.label, item.name, item.id, item.description, item.origin]
1059
.join(" ").toLowerCase().includes(query);
1060
});
1061
},
1062
995
- moveTools(ids, allow) {
996
- for (const id of unique(ids)) movePolicyItem(this.draft.toolPolicy, id, allow);
997
- this.selectedAllowedTools = [];
998
- this.selectedBlockedTools = [];
999
- },
1000
-
1001
- moveAllVisibleTools(allow) {
1002
- return this.confirmBulkMove(
1003
- "tool",
1004
- this.filteredTools(!allow).map((item) => item.id),
1005
- allow,
1006
- );
1007
- },
1008
-
1009
- filteredSkills(allowed) {
1063
+ filteredSkills() {
1064
const query = this.skillSearch.trim().toLowerCase();
1065
return (this.state?.skills?.catalog || []).filter((item) => {
1012
- if (this.draft.skillPolicy.mode !== "custom" && item.available === false) return false;
1013
- if (this.isSkillAllowed(item) !== allowed) return false;
1066
if (this.skillOrigin !== "all" && item.origin !== this.skillOrigin) return false;
1067
return !query || [item.name, item.description, item.origin, ...(item.tags || [])]
1068
.join(" ").toLowerCase().includes(query);
1069
});
1070
},
1071
1020
- moveSkills(ids, allow) {
1021
- for (const id of unique(ids)) movePolicyItem(this.draft.skillPolicy, id, allow);
1022
- this.selectedAllowedSkills = [];
1023
- this.selectedBlockedSkills = [];
1024
- },
1025
-
1026
- moveAllVisibleSkills(allow) {
1027
- return this.confirmBulkMove(
1028
- "skill",
1029
- this.filteredSkills(!allow).map((item) => item.name),
1030
- allow,
1031
- );
1032
- },
1033
-
1034
- async confirmBulkMove(kind, ids, allow) {
1035
- if (!ids.length) return;
1036
- const action = allow ? "Allow" : "Block";
1037
- const plural = `${kind}${ids.length === 1 ? "" : "s"}`;
1038
- const confirmed = await showConfirmDialog({
1039
- title: `${action} ${ids.length} shown ${plural}?`,
1040
- message: `<p>This changes every ${kind} currently shown by your filters.</p>`,
1041
- confirmText: `${action} shown ${plural}`,
1042
- type: "warning",
1043
- });
1044
- if (!confirmed) return;
1045
- if (kind === "tool") this.moveTools(ids, allow);
1046
- else this.moveSkills(ids, allow);
1047
- },
1048
-
1072
skillWarnings(skill) {
1073
const warnings = [];
1074
for (const toolName of skill.allowed_tools || []) {
@@ -1090,7 +1113,10 @@ const model = {
1113
const message = this.draft.creating
1114
? "Instructions are required for a new agent."
1115
: `Instructions can’t be empty. Use ${fallback} instructions instead.`;
1093
- issues.push({ key: "instructions", section: "2", field: "agent-editor-prompt-text", label: "Instructions", message });
1116
+ const field = this.mode === "advanced"
1117
+ ? "agent-editor-prompt-ace"
1118
+ : "agent-editor-instructions";
1119
+ issues.push({ key: "instructions", section: "2", field, label: "Instructions", message });
1120
}
1121
if (this.avatarUploading) {
1122
issues.push({ key: "avatar", section: "1", field: "agent-editor-advanced-name", label: "Avatar", message: "Wait for the avatar upload to finish." });
@@ -1296,7 +1322,7 @@ const model = {
1322
this.plan = data;
1323
this.planStatus = "ready";
1324
this.pendingMutation = { destructive };
1299
- this.setMode("advanced", "5", false);
1325
+ this.setMode("advanced", "6", false);
1326
} catch (error) {
1327
this.error = error.message || String(error);
1328
} finally {
plugins/_agent_editor/webui/main.html
+129
-117
@@ -50,13 +50,13 @@
50
</span>
51
<strong x-text="$store.agentEditor.activeProfile().title || $store.agentEditor.activeProfile().id"></strong>
52
</span>
53
- <button type="button" class="button icon-button" title="Edit" :aria-label="`Edit ${$store.agentEditor.activeProfile().title || $store.agentEditor.activeProfile().id}`" @click="$store.agentEditor.loadEditor($store.agentEditor.activeProfile().id, false)"><x-icon class="icon" name="edit"></x-icon></button>
53
+ <button type="button" class="button icon-button" x-show="$store.agentEditor.activeProfile().id !== 'default'" title="Edit" :aria-label="`Edit ${$store.agentEditor.activeProfile().title || $store.agentEditor.activeProfile().id}`" @click="$store.agentEditor.loadEditor($store.agentEditor.activeProfile().id, false)"><x-icon class="icon" name="edit"></x-icon></button>
54
</div>
55
</template>
56
<button type="button" class="button agent-manager-create" @click="$store.agentEditor.loadEditor('new-agent', true)"><x-icon class="icon" name="add"></x-icon>Create agent</button>
57
</div>
58
<div class="agent-manager-list">
59
- <template x-for="profile in $store.agentEditor.profiles" :key="profile.id">
59
+ <template x-for="profile in $store.agentEditor.visibleProfiles" :key="profile.id">
60
<article class="agent-manager-card">
61
<div class="agent-manager-avatar" :style="`background:${$store.agentEditor.profileVisual(profile).color}`">
62
<img x-show="$store.agentEditor.profileVisual(profile).url" :src="$store.agentEditor.profileVisual(profile).url" :alt="`${profile.title || profile.id} avatar`">
@@ -144,12 +144,26 @@
144
<button type="button" class="text-button" x-show="$store.agentEditor.state.profile.metadata.avatar.has_override || $store.agentEditor.draft.avatar" @click="$store.agentEditor.resetAvatar()">Reset</button>
145
</div>
146
</div>
147
- <div class="agent-field agent-name-field">
148
- <label for="agent-editor-name" class="agent-field-label">Agent name</label>
149
- <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">
150
- <div class="agent-id-feedback" x-show="$store.agentEditor.fieldIssue('name')">
151
- <span id="agent-editor-name-error" class="field-error" role="alert" x-text="$store.agentEditor.fieldIssue('name')?.message"></span>
152
- <button type="button" class="text-button" x-show="$store.agentEditor.profileConflict" @click="$store.agentEditor.openConflictingProfile()">Open existing agent</button>
147
+ <div class="agent-easy-identity-fields">
148
+ <div class="agent-field agent-name-field">
149
+ <label for="agent-editor-name" class="agent-field-label">Agent name</label>
150
+ <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">
151
+ <div class="agent-id-feedback" x-show="$store.agentEditor.fieldIssue('name')">
152
+ <span id="agent-editor-name-error" class="field-error" role="alert" x-text="$store.agentEditor.fieldIssue('name')?.message"></span>
153
+ <button type="button" class="text-button" x-show="$store.agentEditor.profileConflict" @click="$store.agentEditor.openConflictingProfile()">Open existing agent</button>
154
+ </div>
155
+ </div>
156
+ <div class="agent-field agent-model-preset">
157
+ <label for="agent-editor-easy-model-preset" class="agent-field-label">Model preset</label>
158
+ <div class="agent-model-preset-picker">
159
+ <select id="agent-editor-easy-model-preset" x-model="$store.agentEditor.draft.modelPreset">
160
+ <option value="" x-text="`Use current preset (${$store.agentEditor.state.model_preset.effective})`"></option>
161
+ <template x-for="preset in $store.agentEditor.state.model_presets" :key="preset.name">
162
+ <option :value="preset.name" x-text="preset.name"></option>
163
+ </template>
164
+ </select>
165
+ <button type="button" class="button" @click="$store.agentEditor.openPresetManager()"><x-icon class="icon" name="tune"></x-icon>Edit Presets</button>
166
+ </div>
167
</div>
168
</div>
169
</section>
@@ -165,23 +179,52 @@
179
</div>
180
</section>
181
168
- <section class="agent-easy-field agent-easy-tools">
169
- <div class="agent-field-heading">
170
- <div><div class="agent-field-label">Tools <span class="field-count" x-text="`— ${$store.agentEditor.toolCatalog.length} ${$store.agentEditor.toolCatalog.length === 1 ? 'tool' : 'tools'}`"></span></div><p>Choose which tools this agent can use.</p></div>
182
+ <section class="agent-easy-field agent-easy-capabilities">
183
+ <div class="agent-field-heading"><div><div class="agent-field-label">Capabilities</div><p>Choose the default access, or set individual items On or Off.</p></div></div>
184
+ <div class="capability-policy-group">
185
+ <div class="future-policy-control"><strong>Allow tools by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('tool') === 'allow'" @change="$store.agentEditor.setPolicyDefault('tool', $event.target.checked ? 'allow' : 'block')" aria-label="Allow tools by default"><span class="toggler"></span></label></div>
186
+ <details class="capability-accordion">
187
+ <summary><span>Choose individual tools</span><x-icon name="expand_more"></x-icon></summary>
188
+ <div class="policy-items" role="list" aria-label="Tools this agent can use">
189
+ <template x-for="tool in $store.agentEditor.standardToolCatalog" :key="tool.id">
190
+ <div class="policy-item" role="listitem"><div class="policy-item-copy"><strong x-text="tool.label"></strong><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p></div><div class="policy-state-control" role="group" :aria-label="`${tool.label} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'allow'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'default'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('tool') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'block'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'block')">Off</button></div></div>
191
+ </template>
192
+ <p class="policy-empty" x-show="!$store.agentEditor.standardToolCatalog.length">No configurable tools are available.</p>
193
+ </div>
194
+ </details>
195
</div>
172
- <div class="easy-tool-list" role="list" aria-label="Tools this agent can use">
173
- <template x-for="tool in $store.agentEditor.toolCatalog" :key="tool.id">
174
- <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>
175
- </template>
176
- <p class="policy-empty" x-show="!$store.agentEditor.toolCatalog.length">No configurable tools are available.</p>
196
+
197
+ <div class="capability-policy-group">
198
+ <div class="future-policy-control"><strong>Allow MCPs by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('mcp') === 'allow'" @change="$store.agentEditor.setPolicyDefault('mcp', $event.target.checked ? 'allow' : 'block')" aria-label="Allow MCPs by default"><span class="toggler"></span></label></div>
199
+ <details class="capability-accordion">
200
+ <summary><span>Choose individual MCPs</span><x-icon name="expand_more"></x-icon></summary>
201
+ <div class="policy-items" role="list" aria-label="MCP tools this agent can use">
202
+ <template x-for="tool in $store.agentEditor.mcpCatalog" :key="tool.id">
203
+ <div class="policy-item" role="listitem"><div class="policy-item-copy"><strong x-text="tool.label"></strong><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p></div><div class="policy-state-control" role="group" :aria-label="`${tool.label} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'allow'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'default'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('mcp') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'block'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'block')">Off</button></div></div>
204
+ </template>
205
+ <p class="policy-empty" x-show="!$store.agentEditor.mcpCatalog.length">No MCP tools are available.</p>
206
+ </div>
207
+ </details>
208
+ </div>
209
+
210
+ <div class="capability-policy-group">
211
+ <div class="future-policy-control"><strong>Allow skills by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('skill') === 'allow'" @change="$store.agentEditor.setPolicyDefault('skill', $event.target.checked ? 'allow' : 'block')" aria-label="Allow skills by default"><span class="toggler"></span></label></div>
212
+ <details class="capability-accordion">
213
+ <summary><span>Choose individual skills</span><x-icon name="expand_more"></x-icon></summary>
214
+ <div class="policy-items" role="list" aria-label="Skills this agent can use">
215
+ <template x-for="skill in $store.agentEditor.skillCatalog" :key="skill.path">
216
+ <div class="policy-item" role="listitem"><div class="policy-item-copy"><strong x-text="skill.name"></strong><p class="policy-item-description" x-show="skill.description" x-text="skill.description"></p><em x-show="$store.agentEditor.skillWarnings(skill).length" x-text="`Expects blocked tool: ${$store.agentEditor.skillWarnings(skill).join(', ')}`"></em></div><div class="policy-state-control" role="group" :aria-label="`${skill.name} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'allow'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'default'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('skill') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'block'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'block')">Off</button></div></div>
217
+ </template>
218
+ <p class="policy-empty" x-show="!$store.agentEditor.skillCatalog.length">No configurable skills are available.</p>
219
+ </div>
220
+ </details>
221
</div>
178
- <p class="easy-skills-hint">To enable or disable skills, click Advanced.</p>
222
</section>
223
</main>
224
225
<div class="agent-advanced" x-show="$store.agentEditor.mode === 'advanced'">
226
<nav class="agent-advanced-nav" aria-label="Advanced editor sections">
184
- <template x-for="item in [{id:'1',label:'Identity'},{id:'2',label:'Prompt files'},{id:'3',label:'Tools'},{id:'4',label:'Skills'},{id:'5',label:'Review'}]" :key="item.id">
227
+ <template x-for="item in [{id:'1',label:'Identity'},{id:'2',label:'Prompt files'},{id:'3',label:'Tools'},{id:'4',label:'MCPs'},{id:'5',label:'Skills'},{id:'6',label:'Review'}]" :key="item.id">
228
<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)">
229
<span class="section-number" x-text="item.id"></span><span x-text="item.label"></span>
230
<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>
@@ -232,7 +275,7 @@
275
</section>
276
277
<section x-show="$store.agentEditor.section === '2'" data-agent-editor-section="2" tabindex="-1" aria-labelledby="agent-section-2-title">
235
- <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 inherited version next to your version.</p></header>
278
+ <header class="advanced-section-heading"><h3 id="agent-section-2-title">Prompt files</h3><p>Choose a file and edit its prompt.</p></header>
279
<div class="prompt-workspace">
280
<aside class="prompt-browser">
281
<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>
@@ -252,61 +295,47 @@
295
</aside>
296
<div class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0" x-show="$store.agentEditor.selectedPromptDraft">
297
<div class="prompt-editor-header">
255
- <div><strong x-text="$store.agentEditor.selectedPrompt"></strong><div class="source-chain" x-text="$store.agentEditor.promptSourceChain($store.agentEditor.selectedPromptDraft)"></div></div>
298
+ <div class="prompt-file-heading">
299
+ <strong x-text="$store.agentEditor.selectedPrompt"></strong>
300
+ <div class="prompt-customization-path"><code x-text="$store.agentEditor.promptCustomizationPath()"></code><button type="button" class="button" title="Copy customization path" aria-label="Copy customization path" @click="$store.agentEditor.copyPromptPath()"><x-icon name="content_copy"></x-icon></button></div>
301
+ </div>
302
<div class="prompt-actions">
303
<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>
304
<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>
305
<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 default</button>
306
</div>
307
</div>
262
- <div class="prompt-customization-path"><code x-text="$store.agentEditor.promptCustomizationPath()"></code><button type="button" class="button" title="Copy customization path" aria-label="Copy customization path" @click="$store.agentEditor.copyPromptPath()"><x-icon name="content_copy"></x-icon></button></div>
308
<div class="agent-editor-note" 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>
264
- <div class="prompt-view-tabs" role="tablist" aria-label="Prompt view">
265
- <button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === ''" :class="{ active: $store.agentEditor.comparePrompt === '' }" @click="$store.agentEditor.comparePrompt = ''">Your version</button>
266
- <button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === 'inherited'" :class="{ active: $store.agentEditor.comparePrompt === 'inherited' }" @click="$store.agentEditor.comparePrompt = 'inherited'" x-text="$store.agentEditor.inheritedLabel"></button>
267
- <button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === 'compare'" :class="{ active: $store.agentEditor.comparePrompt === 'compare' }" @click="$store.agentEditor.comparePrompt = 'compare'">Compare</button>
268
- </div>
269
- <div class="prompt-find"><label><x-icon name="search"></x-icon><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 prompt-match-action" aria-label="Previous match" @click="$store.agentEditor.findInPrompt(-1)"><x-icon name="keyboard_arrow_up"></x-icon></button><button type="button" class="button icon prompt-match-action" aria-label="Next match" @click="$store.agentEditor.findInPrompt(1)"><x-icon name="keyboard_arrow_down"></x-icon></button></div>
270
- <div class="prompt-panes" :class="{ compare: $store.agentEditor.comparePrompt === 'compare' }">
271
- <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>
272
- <div class="prompt-pane inherited" x-show="$store.agentEditor.comparePrompt"><div class="prompt-pane-title" x-text="$store.agentEditor.inheritedLabel"></div><pre x-text="$store.agentEditor.selectedPromptDraft.inherited || '(empty)'" tabindex="0"></pre></div>
273
- </div>
309
+ <div id="agent-editor-prompt-ace" class="prompt-ace" :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"></div>
310
+ <div class="agent-editor-note" role="alert" x-show="$store.agentEditor.aceUnavailable">Prompt editor is unavailable. Reload Agent Zero and try again.</div>
311
+ <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>
312
</div>
313
</div>
314
</section>
315
316
<section x-show="$store.agentEditor.section === '3'" data-agent-editor-section="3" tabindex="-1" aria-labelledby="agent-section-3-title">
317
<header class="advanced-section-heading"><h3 id="agent-section-3-title">Tools</h3><p>Choose which tools this agent can use.</p></header>
280
- <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><span x-text="$store.agentEditor.projectName ? 'Use inherited tool access' : 'Use standard tool access'"></span> <small x-text="`(${$store.agentEditor.toolCatalog.length} ${$store.agentEditor.toolCatalog.length === 1 ? 'tool' : '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>
281
- <fieldset class="policy-editor" :disabled="$store.agentEditor.draft.toolPolicy.mode !== 'custom'">
282
- <legend class="sr-only">Tool access selection</legend>
283
- <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>
284
- <div class="policy-lists">
285
- <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} ${$store.agentEditor.filteredTools(true).length === 1 ? 'tool' : '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"><label 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></label></template></div></section>
286
- <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>
287
- <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} ${$store.agentEditor.filteredTools(false).length === 1 ? 'tool' : '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"><label 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></label></template></div></section>
288
- </div>
289
- <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>
290
- </fieldset>
318
+ <div class="future-policy-control"><strong>Allow tools by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('tool') === 'allow'" @change="$store.agentEditor.setPolicyDefault('tool', $event.target.checked ? 'allow' : 'block')" aria-label="Allow tools by default"><span class="toggler"></span></label></div>
319
+ <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>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>
320
+ <section class="policy-group" aria-labelledby="tools-title"><header><h4 id="tools-title">Tools</h4><span x-text="$store.agentEditor.filteredTools('tool').length"></span></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredTools('tool').length">No tools match these filters.</p><template x-for="tool in $store.agentEditor.filteredTools('tool')" :key="tool.id"><div class="policy-item"><div class="policy-item-copy"><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 === false">Unavailable — kept in your settings</em></div><div class="policy-state-control" role="group" :aria-label="`${tool.label} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'allow'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'default'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('tool') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'block'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'block')">Off</button></div></div></template></div></section>
321
</section>
322
323
<section x-show="$store.agentEditor.section === '4'" data-agent-editor-section="4" tabindex="-1" aria-labelledby="agent-section-4-title">
294
- <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>
295
- <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><span x-text="$store.agentEditor.projectName ? 'Use inherited skill access' : 'Use standard skill access'"></span> <small x-text="`(${$store.agentEditor.skillCatalog.length} ${$store.agentEditor.skillCatalog.length === 1 ? 'skill' : '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>
296
- <fieldset class="policy-editor" :disabled="$store.agentEditor.draft.skillPolicy.mode !== 'custom'">
297
- <legend class="sr-only">Skill access selection</legend>
298
- <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>
299
- <div class="policy-lists">
300
- <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} ${$store.agentEditor.filteredSkills(true).length === 1 ? 'skill' : '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"><label 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></label></template></div></section>
301
- <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>
302
- <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} ${$store.agentEditor.filteredSkills(false).length === 1 ? 'skill' : '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"><label 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></label></template></div></section>
303
- </div>
304
- <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>
305
- </fieldset>
324
+ <header class="advanced-section-heading"><h3 id="agent-section-4-title">MCPs</h3><p>Choose which MCP tools this agent can use.</p></header>
325
+ <div class="future-policy-control"><strong>Allow MCPs by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('mcp') === 'allow'" @change="$store.agentEditor.setPolicyDefault('mcp', $event.target.checked ? 'allow' : 'block')" aria-label="Allow MCPs by default"><span class="toggler"></span></label></div>
326
+ <div class="policy-filters tools"><label class="policy-search"><x-icon name="search"></x-icon><span class="sr-only">Search MCPs</span><input type="search" x-model="$store.agentEditor.toolSearch" placeholder="Search MCPs"></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>
327
+ <section class="policy-group" aria-labelledby="mcps-title"><header><h4 id="mcps-title">MCPs</h4><span x-text="$store.agentEditor.filteredTools('mcp').length"></span></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredTools('mcp').length">No MCP tools match these filters.</p><template x-for="tool in $store.agentEditor.filteredTools('mcp')" :key="tool.id"><div class="policy-item"><div class="policy-item-copy"><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 === false">Unavailable — kept in your settings</em></div><div class="policy-state-control" role="group" :aria-label="`${tool.label} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'allow'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'default'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('mcp') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'block'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'block')">Off</button></div></div></template></div></section>
328
</section>
329
330
<section x-show="$store.agentEditor.section === '5'" data-agent-editor-section="5" tabindex="-1" aria-labelledby="agent-section-5-title">
309
- <header class="advanced-section-heading"><h3 id="agent-section-5-title">Review</h3><p>Saving will change exactly these files — nothing else.</p></header>
331
+ <header class="advanced-section-heading"><h3 id="agent-section-5-title">Skills</h3><p>Choose which skills this agent can find and use.</p></header>
332
+ <div class="future-policy-control"><strong>Allow skills by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('skill') === 'allow'" @change="$store.agentEditor.setPolicyDefault('skill', $event.target.checked ? 'allow' : 'block')" aria-label="Allow skills by default"><span class="toggler"></span></label></div>
333
+ <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>
334
+ <section class="policy-group" aria-labelledby="skills-title"><header><h4 id="skills-title">Skills</h4><span x-text="$store.agentEditor.filteredSkills().length"></span></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredSkills().length">No skills match these filters.</p><template x-for="skill in $store.agentEditor.filteredSkills()" :key="skill.path"><div class="policy-item"><div class="policy-item-copy"><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 class="policy-state-control" role="group" :aria-label="`${skill.name} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'allow'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'default'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('skill') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'block'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'block')">Off</button></div></div></template></div></section>
335
+ </section>
336
+
337
+ <section x-show="$store.agentEditor.section === '6'" data-agent-editor-section="6" tabindex="-1" aria-labelledby="agent-section-6-title">
338
+ <header class="advanced-section-heading"><h3 id="agent-section-6-title">Review</h3><p>Saving will change exactly these files — nothing else.</p></header>
339
<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>
340
<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>
341
<div class="review-blocked" role="alert" x-show="$store.agentEditor.planStatus === 'blocked'">
@@ -366,11 +395,7 @@
395
.agent-editor h2,.agent-editor h3,.agent-editor h4,.agent-editor p { margin: 0; }
396
.agent-editor button,.agent-editor input,.agent-editor textarea,.agent-editor select { font: inherit; }
397
.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); }
369
- .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); }
370
- .agent-editor input[type="checkbox"] { display:grid; place-content:center; border-radius:4px; }
371
- .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); }
372
- .agent-editor input[type="checkbox"]:checked { border-color:var(--agent-editor-action); background:var(--agent-editor-action); }
373
- .agent-editor input[type="checkbox"]:checked::before { opacity:1; }
398
+ .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); }
399
.agent-editor input[type="radio"] { border-radius:50%; }
400
.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); }
401
.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)); }
@@ -390,7 +415,8 @@
415
.agent-mode-switch button { border:0; border-radius:6px; padding:.42rem .8rem; color:var(--color-text-secondary); background:transparent; }
416
.agent-mode-switch button.active { color:var(--color-text); background:var(--color-panel); box-shadow:0 1px 4px rgba(0,0,0,.2); }
417
.agent-easy { max-width:43rem; margin:0 auto; display:flex; flex-direction:column; gap:1.35rem; padding:.25rem .25rem 1rem; }
393
- .agent-easy-identity { position:relative; display:grid; grid-template-columns:7.2rem 1fr; gap:1rem; align-items:center; }
418
+ .agent-easy-identity { position:relative; display:grid; grid-template-columns:7.2rem 1fr; gap:1rem; align-items:start; }
419
+ .agent-easy-identity-fields { width:100%; min-width:0; display:flex; flex-direction:column; gap:.9rem; }
420
.agent-avatar-wrap { display:flex; flex-direction:column; align-items:center; gap:.45rem; }
421
.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); }
422
.agent-avatar img { width:100%; height:100%; object-fit:cover; }
@@ -401,9 +427,9 @@
427
.avatar-action-icon x-icon { font-size:1.05rem; }
428
.avatar-color-action input,.avatar-upload-action input { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }
429
.agent-field { display:flex; flex-direction:column; gap:.35rem; min-width:0; }
404
- .agent-name-field { align-self:start; }
430
+ .agent-name-field { width:100%; }
431
.agent-field-label { font-weight:650; font-size:.92rem; }
406
- .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; }
432
+ .agent-field input,.agent-field textarea,.agent-easy textarea,.policy-filters input,.policy-filters select,.compact-search input { width:100%; box-sizing:border-box; }
433
.agent-field small,.agent-field-heading p,.field-status { color:var(--color-text-secondary); font-size:.79rem; }
434
.field-count { color:var(--color-text-secondary); font-size:.79rem; font-weight:400; }
435
.field-status { display:block; margin-top:.15rem; }
@@ -415,16 +441,22 @@
441
.agent-editor .text-button:hover { text-decoration:underline; }
442
.agent-easy textarea { min-height:11rem; resize:vertical; }
443
.restore-action { --agent-editor-action:var(--color-primary); display:inline-flex; align-items:center; gap:.25rem; margin-top:.4rem; }
418
- .easy-tool-list { display:flex; flex-direction:column; padding:.2rem .35rem .2rem 0; }
419
- .easy-tool-list .policy-item { cursor:pointer; }
420
- .easy-skills-hint { margin:.55rem 0 0; color:var(--color-text-secondary); font-size:.79rem; }
421
- .agent-advanced { display:grid; grid-template-columns:14rem minmax(0,1fr); gap:1rem; min-height:0; }
444
+ .agent-easy-capabilities { display:flex; flex-direction:column; gap:.65rem; }
445
+ .future-policy-control { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:.45rem .2rem; }
446
+ .capability-policy-group + .capability-policy-group { border-top:1px solid var(--color-border); padding-top:.55rem; }
447
+ .capability-accordion summary { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.65rem .2rem .75rem; cursor:pointer; color:var(--color-text-secondary); font-weight:500; }
448
+ .capability-accordion summary x-icon { color:var(--color-text-secondary); transition:transform .15s ease; }
449
+ .capability-accordion[open] summary x-icon { transform:rotate(180deg); }
450
+ .capability-accordion .policy-items { max-height:none; overflow:visible; padding:0 0 .45rem; }
451
+ .agent-advanced { flex:1 1 auto; display:grid; grid-template-columns:14rem minmax(0,1fr); gap:1rem; min-height:0; }
452
.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; }
453
.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; }
454
.agent-advanced-nav button.active { color:var(--color-text); background:var(--color-panel); }
455
.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; }
456
.agent-advanced-content,.agent-editor-workspace,.agent-manager { min-width:0; }
457
+ .agent-advanced-content { min-height:0; overflow:auto; }
458
.agent-advanced-content > section { outline:none; display:flex; flex-direction:column; gap:1rem; }
459
+ .agent-advanced-content > section[data-agent-editor-section="2"] { height:100%; min-height:0; }
460
.advanced-section-heading { display:flex; flex-direction:column; gap:.25rem; padding-bottom:.8rem; border-bottom:1px solid var(--color-border); }
461
.advanced-section-heading h3 { font-size:1.2rem; }
462
.advanced-section-heading p { max-width:48rem; color:var(--color-text-secondary); font-size:.84rem; }
@@ -439,12 +471,12 @@
471
.agent-model-preset-picker { width:100%; display:grid; grid-template-columns:minmax(13rem,18rem) auto; align-items:center; justify-content:space-between; gap:1rem; }
472
.agent-model-preset-picker select { width:100%; }
473
.agent-model-preset-picker > .button { display:inline-flex; align-items:center; gap:.35rem; }
442
- .prompt-workspace { display:grid; grid-template-columns:minmax(13rem,16rem) minmax(0,1fr); gap:.8rem; min-width:0; height:clamp(34rem,65vh,58rem); }
474
+ .prompt-workspace { flex:1 1 auto; display:grid; grid-template-columns:minmax(13rem,16rem) minmax(0,1fr); gap:.8rem; min-width:0; min-height:0; }
475
.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; }
444
- .compact-search,.policy-search,.prompt-find label { display:grid; grid-template-columns:1.25rem minmax(0,1fr); align-items:center; gap:.3rem; min-width:0; height:2.125rem; padding:0 .45rem; border:1px solid color-mix(in srgb,var(--color-border) 42%,transparent); border-radius:7px; background:color-mix(in srgb,var(--color-text) 10%,transparent); color:var(--color-text-secondary); }
476
+ .compact-search,.policy-search { display:grid; grid-template-columns:1.25rem minmax(0,1fr); align-items:center; gap:.3rem; min-width:0; height:2.125rem; padding:0 .45rem; border:1px solid color-mix(in srgb,var(--color-border) 42%,transparent); border-radius:7px; background:color-mix(in srgb,var(--color-text) 10%,transparent); color:var(--color-text-secondary); }
477
.compact-search { margin:.45rem; }
446
- .compact-search:focus-within,.policy-search:focus-within,.prompt-find label:focus-within { border-color:color-mix(in srgb,var(--color-primary) 46%,var(--color-border)); background:color-mix(in srgb,var(--color-text) 13%,transparent); }
447
- .agent-editor .compact-search input,.agent-editor .policy-search input,.agent-editor .prompt-find label input { height:100%; min-height:0; padding:0; border:0; outline:0; appearance:none; background:transparent; }
478
+ .compact-search:focus-within,.policy-search:focus-within { border-color:color-mix(in srgb,var(--color-primary) 46%,var(--color-border)); background:color-mix(in srgb,var(--color-text) 13%,transparent); }
479
+ .agent-editor .compact-search input,.agent-editor .policy-search input { height:100%; min-height:0; padding:0; border:0; outline:0; appearance:none; background:transparent; }
480
.prompt-file-list { flex:1; overflow:auto; padding:.35rem; }
481
.prompt-file-group { min-width:0; margin-bottom:.55rem; }
482
.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; }
@@ -453,57 +485,36 @@
485
.prompt-file-name { grid-column:1/-1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:monospace; font-size:.76rem; }
486
.prompt-file-state { color:var(--color-text-secondary); font-size:.7rem; }
487
.prompt-empty { padding:.65rem; color:var(--color-text-secondary); font-size:.78rem; }
456
- .prompt-editor { display:flex; flex-direction:column; gap:.55rem; min-width:0; max-width:100%; max-height:100%; overflow:auto; }
488
+ .prompt-editor { display:flex; flex-direction:column; gap:.55rem; min-width:0; min-height:0; max-width:100%; overflow:hidden; }
489
.prompt-editor-header { display:flex; justify-content:space-between; gap:.7rem; align-items:flex-start; }
490
.prompt-editor-header > div:first-child { min-width:0; overflow-wrap:anywhere; }
459
- .source-chain { margin-top:.2rem; color:var(--color-text-secondary); font-size:.75rem; }
491
+ .prompt-file-heading { display:flex; flex-direction:column; gap:.25rem; }
492
.prompt-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:.35rem; min-width:0; }
493
.prompt-actions .button { max-width:100%; white-space:normal; overflow-wrap:anywhere; }
462
- .prompt-customization-path { display:flex; align-items:center; justify-content:flex-end; gap:.4rem; min-width:0; }
463
- .prompt-customization-path code { min-width:0; color:var(--color-text-secondary); font-size:.72rem; overflow-wrap:anywhere; text-align:right; }
494
+ .prompt-customization-path { display:flex; align-items:center; gap:.4rem; min-width:0; }
495
+ .prompt-customization-path code { min-width:0; color:var(--color-text-secondary); font-size:.72rem; overflow-wrap:anywhere; }
496
.prompt-customization-path .button { width:2.125rem; height:2.125rem; flex:0 0 auto; padding:0; }
465
- .prompt-view-tabs { display:flex; flex-wrap:wrap; gap:.25rem; padding-bottom:.35rem; border-bottom:1px solid var(--color-border); }
466
- .prompt-view-tabs button { min-height:2rem; padding:.35rem .65rem; border:0; border-radius:6px; background:transparent; color:var(--color-text-secondary); }
467
- .prompt-view-tabs button.active { background:var(--color-input); color:var(--color-text); }
468
- .prompt-find { display:flex; flex-wrap:wrap; align-items:center; gap:.35rem; min-width:0; }
469
- .prompt-find label { flex:1 1 10rem; min-width:0; }
470
- .prompt-find span { color:var(--color-text-secondary); font-size:.75rem; white-space:nowrap; }
471
- .prompt-find .prompt-match-action { flex:0 0 2.125rem; width:2.125rem; height:2.125rem; min-height:2.125rem; padding:0; border-radius:7px; justify-content:center; }
472
- .prompt-panes { display:grid; grid-template-columns:1fr; gap:.65rem; min-height:18rem; }
473
- .prompt-panes.compare { grid-template-columns:1fr 1fr; }
474
- .prompt-pane { min-width:0; min-height:0; display:flex; flex-direction:column; gap:.35rem; }
475
- .prompt-pane label,.prompt-pane-title { color:var(--color-text-secondary); font-size:.75rem; }
476
- .prompt-pane textarea,.prompt-pane pre { flex:1; box-sizing:border-box; width:100%; min-height:0; 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; }
477
- .prompt-pane textarea:focus-visible { outline-offset:-2px; }
478
- .prompt-pane.inherited pre { opacity:.86; }
497
+ .prompt-ace { flex:1; min-height:0; border:1px solid var(--color-border); border-radius:8px; overflow:hidden; font:13px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace; }
498
.profile-maintenance summary { display:flex; align-items:center; min-height:1.5rem; cursor:pointer; }
480
- .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; }
481
- .policy-mode-row label,.future-default label { display:flex; gap:.35rem; align-items:center; }
482
- .policy-mode-row small { color:var(--color-text-secondary); }
483
- .policy-editor { min-width:0; margin:.75rem 0 0; padding:0; border:0; transition:opacity .15s ease; }
484
- .policy-editor:disabled { opacity:.48; }
485
- .future-default { margin-top:.8rem; }
486
- .future-default legend { padding:0 .35rem; color:var(--color-text-secondary); font-size:.8rem; }
487
- .policy-filters { display:grid; grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto) minmax(8rem,auto); gap:.6rem; margin-bottom:.7rem; }
499
+ .policy-filters { display:grid; grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto); gap:.6rem; }
500
.policy-filters.skills { grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto); }
501
.policy-filters label { display:flex; align-items:center; gap:.35rem; font-size:.78rem; color:var(--color-text-secondary); }
502
.policy-filters select { flex:1; }
491
- .policy-lists { display:grid; grid-template-columns:minmax(0,1fr) 2.5rem minmax(0,1fr); gap:.55rem; }
492
- .policy-transfer-actions { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.5rem; }
493
- .policy-transfer-actions .button { width:2.5rem; height:2.5rem; padding:0; justify-content:center; }
494
- .policy-list { display:flex; flex-direction:column; min-width:0; min-height:28rem; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
495
- .policy-list header { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.5rem; min-height:3rem; box-sizing:border-box; padding:.7rem; border-bottom:1px solid var(--color-border); background:var(--color-input); }
496
- .policy-list h4 { display:inline; margin-right:.35rem; }
497
- .policy-list header span { color:var(--color-text-secondary); font-size:.72rem; }
498
- .policy-items { flex:1; max-height:32rem; overflow:auto; padding:.35rem; }
499
- .policy-item { display:flex; gap:.55rem; align-items:flex-start; padding:.55rem; border-radius:7px; }
503
+ .policy-group { display:flex; flex-direction:column; min-width:0; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
504
+ .policy-group header { display:flex; align-items:center; justify-content:space-between; gap:.5rem; padding:.65rem .8rem; border-bottom:1px solid var(--color-border); background:var(--color-input); }
505
+ .policy-group header span { color:var(--color-text-secondary); font-size:.72rem; }
506
+ .policy-items { flex:1; max-height:24rem; overflow:auto; padding:.35rem; }
507
+ .policy-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:.75rem; align-items:start; padding:.55rem; border-radius:7px; }
508
.policy-item:hover { background:var(--color-input); }
501
- .policy-item > div { display:flex; flex:1; flex-direction:column; gap:.15rem; min-width:0; }
509
+ .policy-item-copy { display:flex; flex-direction:column; gap:.15rem; min-width:0; }
510
.policy-items small { color:var(--color-text-secondary); overflow-wrap:anywhere; }
511
.policy-items em { color:var(--color-warning); font-size:.72rem; font-style:normal; }
512
.policy-item-description { margin:0; color:var(--color-text-secondary); font-size:.75rem; white-space:pre-wrap; overflow-wrap:anywhere; }
513
+ .policy-state-control { display:inline-flex; align-self:center; overflow:hidden; border:1px solid var(--color-border); border-radius:var(--border-radius-sm); background:var(--color-input); }
514
+ .policy-state-control button { min-height:2rem; padding:.35rem .6rem; border:0; border-left:1px solid var(--color-border); border-radius:0; background:transparent; color:var(--color-text-secondary); cursor:pointer; font-size:.74rem; white-space:nowrap; }
515
+ .policy-state-control button:first-child { border-left:0; }
516
+ .policy-state-control button.active { color:var(--color-text); background:color-mix(in srgb,var(--agent-editor-action) 16%,var(--color-input)); box-shadow:inset 0 0 0 1px var(--agent-editor-action); }
517
.policy-empty { padding:1rem .7rem; color:var(--color-text-secondary); font-size:.8rem; text-align:center; }
506
- .agent-editor .policy-bulk { min-height:1.5rem; padding:.1rem .45rem; font-size:.72rem; line-height:1.15; white-space:normal; }
518
.review-identity { display:grid; grid-template-columns:3.25rem minmax(0,1fr); align-items:center; gap:.8rem; padding:.8rem; border-bottom:1px solid var(--color-border); }
519
.review-identity-avatar { width:3.25rem; aspect-ratio:1; display:grid; place-items:center; overflow:hidden; border-radius:10px; color:#fff; font-weight:700; }
520
.review-identity-avatar img { width:100%; height:100%; object-fit:cover; }
@@ -562,9 +573,9 @@
573
.agent-editor :focus-visible { outline:2px solid var(--agent-editor-action); outline-offset:2px; }
574
.modal-inner.agent-editor-easy { width:min(720px,calc(100vw - 2rem)); max-width:720px; }
575
.modal-inner.agent-editor-advanced { width:92vw; max-width:none; height:min(calc(100vh - 4rem),80rem); max-height:calc(100vh - 4rem); }
576
+ .modal-inner.agent-editor-advanced .modal-scroll,.modal-inner.agent-editor-advanced .modal-bd,.modal-inner.agent-editor-advanced .agent-editor,.modal-inner.agent-editor-advanced .agent-editor-workspace { display:flex; flex:1 1 auto; min-height:0; }
577
.modal-inner.agent-editor-advanced .modal-scroll { max-height:none; }
566
- .modal-inner.agent-editor-advanced .modal-bd { min-height:0; }
567
- .modal-inner.agent-editor-advanced .agent-editor,.modal-inner.agent-editor-advanced .agent-editor-workspace { min-height:100%; }
578
+ .modal-inner.agent-editor-advanced .agent-editor,.modal-inner.agent-editor-advanced .agent-editor-workspace { flex-direction:column; }
579
@media (max-width: 760px) {
580
.modal-inner.agent-editor-easy,.modal-inner.agent-editor-advanced { width:100vw; max-width:none; height:100vh; max-height:none; border-radius:0; }
581
.agent-editor-topbar { align-items:flex-start; flex-wrap:wrap; }
@@ -572,21 +583,22 @@
583
.agent-editor-topbar-actions { width:100%; flex-wrap:wrap; justify-content:flex-end; }
584
.agent-editor-scope { flex:1 1 14rem; }
585
.agent-easy-identity { grid-template-columns:1fr; justify-items:center; padding-top:1.8rem; }
575
- .agent-name-field { width:100%; }
586
.agent-id-feedback { width:100%; }
587
.agent-advanced { grid-template-columns:1fr; }
588
.agent-advanced-nav { position:static; flex-direction:row; overflow-x:auto; }
589
.agent-advanced-nav button { grid-template-columns:auto auto auto; white-space:nowrap; }
580
- .agent-model-preset-picker { grid-template-columns:1fr; gap:.5rem; }
581
- .prompt-workspace { grid-template-columns:1fr; height:auto; }
590
+ .agent-model-preset-picker { grid-template-columns:minmax(0,1fr) auto; gap:.5rem; }
591
+ .prompt-workspace { grid-template-columns:1fr; }
592
.prompt-browser { height:22rem; max-height:22rem; }
593
.prompt-editor { max-height:none; overflow:visible; }
584
- .prompt-panes.compare,.policy-lists,.change-plan,.advanced-identity-grid,.identity-fields { grid-template-columns:1fr; }
585
- .policy-lists .policy-transfer-actions { flex-direction:row; }
586
- .policy-lists .policy-transfer-actions x-icon { transform:rotate(90deg); }
594
+ .prompt-ace { min-height:18rem; }
595
+ .change-plan,.advanced-identity-grid,.identity-fields { grid-template-columns:1fr; }
596
.identity-fields .agent-field.wide { grid-column:1; }
597
.policy-filters { grid-template-columns:1fr; }
589
- .policy-list { min-height:20rem; }
598
+ .future-policy-control { align-items:flex-start; }
599
+ .policy-item { grid-template-columns:1fr; }
600
+ .policy-state-control { width:100%; align-self:stretch; }
601
+ .policy-state-control button { flex:1; }
602
.agent-manager-list-header { flex-wrap:wrap; }
603
.active-agent-display { width:100%; }
604
.agent-manager-create { margin-left:auto; }
plugins/_tool_access/AGENTS.md
+2
@@ -16,6 +16,8 @@
16
profile `config.json` files, projects may own project or project-profile
17
configs through the standard plugin scope paths, and the runtime remains
18
authoritative.
19
+- Custom configuration stores independent `default` and `mcp_default`
20
+ fallbacks; explicit canonical IDs remain shared in `allowed` and `blocked`.
21
- Required final-response capability is never disabled.
22
23
## Verification
plugins/_tool_access/default_config.yaml
+1
@@ -1,4 +1,5 @@
1
mode: inherit
2
default: allow
3
+mcp_default: allow
4
allowed: []
5
blocked: []
tests/test_agent_editor.py
+41
-1
@@ -329,6 +329,7 @@ def test_plugin_configs_preserve_unowned_keys_and_use_json(user_root: Path) -> N
329
"tool_policy": {
330
"mode": "custom",
331
"default": "block",
332
+ "mcp_default": "allow",
333
"allowed": ["local:search_engine"],
334
"blocked": ["local:shell"],
335
},
@@ -344,7 +345,7 @@ def test_plugin_configs_preserve_unowned_keys_and_use_json(user_root: Path) -> N
345
346
assert json.loads(model.read_text())["manual"] == 1
347
assert set(json.loads(tools.read_text())) >= {
347
- "manual", "mode", "default", "allowed", "blocked"
348
+ "manual", "mode", "default", "mcp_default", "allowed", "blocked"
349
}
350
skill_data = json.loads(skill.read_text())
351
assert skill_data["active_skills"] == [{"name": "existing"}]
@@ -379,6 +380,7 @@ def test_model_and_off_tool_choices_write_only_their_json_contracts(
380
assert json.loads(off.changes[tool_path].content) == {
381
"mode": "custom",
382
"default": "block",
383
+ "mcp_default": "block",
384
"allowed": [],
385
"blocked": [],
386
}
@@ -419,6 +421,44 @@ def test_project_tool_policy_reads_effective_access_and_writes_project_scope(
421
]
422
423
424
+def test_tri_state_tool_mcp_and_skill_policies_write_at_both_scopes(
425
+ user_root: Path,
426
+ project_scope: tuple[editor._EditorContext, Path],
427
+) -> None:
428
+ tool_policy = {
429
+ "mode": "custom",
430
+ "default": "block",
431
+ "mcp_default": "allow",
432
+ "allowed": ["local:shell"],
433
+ "blocked": ["mcp:docs:write"],
434
+ }
435
+ skill_policy = {
436
+ "mode": "custom",
437
+ "default": "allow",
438
+ "allowed": ["Research"],
439
+ "blocked": ["Unsafe"],
440
+ }
441
+ patch = {
442
+ "profile_id": "researcher",
443
+ "tool_policy": tool_policy,
444
+ "skill_policy": skill_policy,
445
+ }
446
+ context, project_agents = project_scope
447
+
448
+ for plan, root in (
449
+ (editor.build_change_plan(patch), user_root),
450
+ (editor.build_change_plan(patch, context), project_agents),
451
+ ):
452
+ editor.apply_change_plan(plan)
453
+ profile_root = root / "researcher" / "plugins"
454
+ assert json.loads(
455
+ (profile_root / "_tool_access" / "config.json").read_text()
456
+ ) == tool_policy
457
+ assert json.loads(
458
+ (profile_root / "_skills" / "config.json").read_text()
459
+ )["visibility_policy"] == skill_policy
460
+
461
+
462
def test_project_agents_are_scope_owned_and_never_leak_global_writes(
463
user_root: Path,
464
project_scope: tuple[editor._EditorContext, Path],
tests/test_agent_editor_webui.py
+195
-98
@@ -33,11 +33,16 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
33
modal,
34
re.DOTALL,
35
).group(0)
36
- skill_section = re.search(
36
+ mcp_section = re.search(
37
r'data-agent-editor-section="4".*?(?=<section x-show="\$store\.agentEditor\.section === \'5\'")',
38
modal,
39
re.DOTALL,
40
).group(0)
41
+ skill_section = re.search(
42
+ r'data-agent-editor-section="5".*?(?=<section x-show="\$store\.agentEditor\.section === \'6\'")',
43
+ modal,
44
+ re.DOTALL,
45
+ ).group(0)
46
easy_surface = modal.split('<div class="agent-advanced"', 1)[0]
47
48
assert "Create agent" not in switcher
@@ -59,43 +64,50 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
64
"Identity",
65
"Prompt files",
66
"Tools",
67
+ "MCPs",
68
"Skills",
69
"Review",
70
"Save & test",
71
)
72
)
73
assert 'aria-label="Editor mode"' in modal
68
- assert "Allow selected" in modal and "Block selected" in modal
74
+ assert "Allow selected" not in modal and "Block selected" not in modal
75
assert "No optional tools" not in modal
76
assert 'class="agent-model-preset-picker"' in modal
71
- assert 'x-model="$store.agentEditor.draft.modelPreset"' in modal
77
+ assert modal.count('x-model="$store.agentEditor.draft.modelPreset"') == 2
78
+ assert 'id="agent-editor-easy-model-preset"' in easy_surface
79
+ easy_identity = easy_surface[easy_surface.index('<section class="agent-easy-identity">'):easy_surface.index('<section class="agent-easy-field">')]
80
+ assert easy_identity.index('id="agent-editor-name"') < easy_identity.index('id="agent-editor-easy-model-preset"')
81
+ assert easy_surface.index('id="agent-editor-easy-model-preset"') < easy_surface.index('id="agent-editor-instructions"')
82
assert '`Use current preset (${$store.agentEditor.state.model_preset.effective})`' in modal
83
assert 'x-for="preset in $store.agentEditor.state.model_presets"' in modal
74
- assert "Edit Presets" in modal
84
+ assert modal.count("Edit Presets") == 2
85
assert "Manage presets" not in modal
86
assert "model-preset-row" not in modal
77
- assert 'class="easy-tool-details"' not in modal
78
- assert 'x-for="tool in $store.agentEditor.toolCatalog"' in easy_surface
79
- assert 'class="field-count"' in easy_surface
80
- assert "toolCatalog.length === 1 ? 'tool' : 'tools'" in modal
81
- assert "skillCatalog.length === 1 ? 'skill' : 'skills'" in modal
82
- assert ':checked="$store.agentEditor.isToolAllowed(tool)"' in easy_surface
83
- assert "$store.agentEditor.setEasyToolAllowed(tool.id, $event.target.checked)" in easy_surface
87
+ assert easy_surface.count('<details class="capability-accordion">') == 3
88
+ assert re.findall(r'<summary><span>(Choose individual (?:tools|MCPs|skills))</span><x-icon name="expand_more"></x-icon></summary>', easy_surface) == ["Choose individual tools", "Choose individual MCPs", "Choose individual skills"]
89
+ assert '<details class="capability-accordion" open' not in easy_surface
90
+ assert 'x-for="tool in $store.agentEditor.standardToolCatalog"' in easy_surface
91
+ assert 'x-for="skill in $store.agentEditor.skillCatalog"' in easy_surface
92
+ assert 'x-for="tool in $store.agentEditor.mcpCatalog"' in easy_surface
93
assert "Choose tools in Advanced" not in modal
85
- assert "To enable or disable skills, click Advanced." in easy_surface
86
- assert 'x-for="skill in $store.agentEditor' not in easy_surface
87
- assert 'class="easy-tool-actions"' not in modal
88
- assert 'class="policy-editor" :disabled="$store.agentEditor.draft.toolPolicy.mode !== \'custom\'"' in tool_section
89
- assert 'class="policy-editor" :disabled="$store.agentEditor.draft.skillPolicy.mode !== \'custom\'"' in skill_section
90
- assert 'class="policy-lists"' in tool_section and 'class="policy-lists"' in skill_section
91
- assert 'aria-label="Block selected tools"' in tool_section
92
- assert 'aria-label="Allow selected tools"' in tool_section
93
- assert 'aria-label="Block selected skills"' in skill_section
94
- assert 'aria-label="Allow selected skills"' in skill_section
94
+ assert "Use standard tool access" not in modal
95
+ assert "Use standard skill access" not in modal
96
+ assert 'class="policy-lists"' not in modal
97
+ assert "policy-transfer-actions" not in modal
98
+ assert tool_section.count('class="policy-group"') == 1
99
+ assert mcp_section.count('class="policy-group"') == 1
100
+ assert skill_section.count('class="policy-group"') == 1
101
+ assert '<label>Category ' not in tool_section
102
+ assert "indeterminate" not in modal
103
+ assert modal.count('class="policy-state-control" role="group"') == 6
104
+ assert modal.count(':aria-pressed="$store.agentEditor.policyItemState') == 18
105
assert '<details class="policy-description"' not in tool_section
106
+ assert '<details class="policy-description"' not in mcp_section
107
assert '<details class="policy-description"' not in skill_section
97
- assert tool_section.count('class="policy-item-description"') == 2
98
- assert skill_section.count('class="policy-item-description"') == 2
108
+ assert tool_section.count('class="policy-item-description"') == 1
109
+ assert mcp_section.count('class="policy-item-description"') == 1
110
+ assert skill_section.count('class="policy-item-description"') == 1
111
assert "Your changes override the built-in profile. The original files stay unchanged." in modal
112
assert 'x-model="$store.agentEditor.projectName"' in modal
113
assert 'x-init="$nextTick(() => $el.value = $store.agentEditor.projectName)"' in modal
@@ -148,7 +160,11 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
160
assert "Create agents and customize inherited profiles" not in modal
161
assert "Unavailable — kept in your settings" in modal
162
assert "Customize this file" not in modal
151
- assert 'role="tablist" aria-label="Prompt view"' in modal
163
+ assert "Choose a file and edit its prompt." in modal
164
+ assert "inherited version next to your version" not in modal
165
+ assert 'role="tablist" aria-label="Prompt view"' not in modal
166
+ assert "Your version" not in modal and ">Compare</button>" not in modal
167
+ assert "Find in file" not in modal and "prompt-match-action" not in modal
168
assert "No prompt files match your search." in modal
169
assert "Saving will change exactly these files — nothing else." in modal
170
assert "Review & test" not in modal
@@ -161,15 +177,21 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
177
assert "<h2" not in modal
178
assert "Section 1" not in modal
179
assert '<textarea id="agent-editor-description" rows="2"' in modal
164
- assert "When new tools are installed later" in modal
165
- assert "When new skills are installed later" in modal
166
- assert "Block until reviewed" in modal
167
- assert "No blocked tools" in modal and "No blocked skills" in modal
180
+ assert "Allow newly installed" not in modal
181
+ assert modal.count("<strong>Allow tools by default</strong>") == 2
182
+ assert modal.count("<strong>Allow MCPs by default</strong>") == 2
183
+ assert modal.count("<strong>Allow skills by default</strong>") == 2
184
+ assert "Allow tools and MCPs by default" not in modal
185
+ assert "Applies to Tools and MCPs left on Default." not in modal
186
+ assert "Applies to Skills left on Default." not in modal
187
+ assert "No blocked tools" not in modal and "No blocked skills" not in modal
188
assert "policy-description" not in modal and "-webkit-line-clamp:2" not in modal
189
assert 'class="prompt-file-list" role="region" aria-label="Prompt file list" tabindex="0"' in modal
190
assert 'class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0"' in modal
191
assert "Preview combined prompt" not in modal
192
assert 'class="prompt-customization-path"' in modal
193
+ assert '<strong x-text="$store.agentEditor.selectedPrompt"></strong>\n <div class="prompt-customization-path">' in modal
194
+ assert "source-chain" not in modal and "promptSourceChain" not in store
195
assert "$store.agentEditor.promptCustomizationPath()" in modal
196
assert 'class="review-identity"' in modal
197
assert 'aria-label="Agent identity"' in modal
@@ -178,10 +200,16 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
200
assert "draft.profileId\"></code>" not in review_identity
201
assert "width:92vw" in modal
202
assert ".modal-inner.agent-editor-advanced .modal-scroll { max-height:none; }" in modal
203
+ assert '.agent-advanced-content > section[data-agent-editor-section="2"] { height:100%; min-height:0; }' in modal
204
+ assert ".prompt-workspace { flex:1 1 auto;" in modal
205
assert 'width:1.5rem; height:1.5rem' in modal
182
- assert modal.count('class="button icon prompt-match-action"') == 2
183
- assert "min-height:3rem" in modal and "font-size:.72rem" in modal
184
- assert "moveAllVisibleTools(false)" in modal and "moveAllVisibleSkills(false)" in modal
206
+ assert 'id="agent-editor-prompt-ace"' in modal
207
+ assert 'id="agent-editor-prompt-text"' not in modal
208
+ assert "globalThis.ace.edit(container)" in store
209
+ assert 'editor.session.setMode("ace/mode/markdown")' in store
210
+ assert "editor.session.setUseWrapMode(true)" in store
211
+ assert "showPrintMargin: false" in store and "useWorker: false" in store
212
+ assert "findInPrompt" not in store and "promptTextSearch" not in store
213
assert 'class="agent-editor-heading"' in modal
214
assert ':aria-invalid=' in modal
215
assert modal.count('role="alert"') >= 4
@@ -191,9 +219,8 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
219
assert 'x-show="$store.agentEditor.view === \'editor\' && !$store.agentEditor.draft?.creating"' in modal
220
assert 'class="btn btn-ok" x-show="$store.agentEditor.view === \'editor\'"' in modal
221
assert "Delete all customizations in" in modal
194
- assert 'input[type="checkbox"]' in modal and "appearance:none" in modal
222
+ assert '.agent-editor input[type="checkbox"]' not in modal
223
assert 'promptDisplayState(prompt)' in modal
196
- assert 'promptSourceChain($store.agentEditor.selectedPromptDraft)' in modal
224
assert "Will reset to default on save." in modal
225
assert "metadataProvenance('description')" not in modal
226
assert 'x-show="$store.agentEditor.metadataProvenance(\'title\')"' in modal
@@ -203,7 +230,7 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
230
assert 'aria-label="Discard current edit"' in modal
231
assert 'aria-label="Accept current edit"' in modal
232
assert ':readonly="!$store.agentEditor.isPromptEditing' not in modal
206
- assert ".prompt-pane textarea:focus-visible { outline-offset:-2px; }" in modal
233
+ assert ".prompt-ace { flex:1; min-height:0;" in modal
234
store_source = STORE.read_text(encoding="utf-8")
235
assert "cannot be recovered" in store_source
236
assert "deletionImpactHtml" not in store_source
@@ -213,34 +240,39 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
240
switcher_mixin = SWITCHER_MIXIN.read_text(encoding="utf-8")
241
assert "avatar_url" in switcher_mixin
242
assert "BUILT_IN_AGENT_COLORS" in switcher_mixin
243
+ assert '!["_example", "default"].includes(profile.id)' in switcher_mixin
244
+ assert 'activeKey !== "default"' in switcher_mixin
245
assert "customized: !!profile.has_user_overrides" not in switcher_mixin
246
+ assert 'x-for="profile in $store.agentEditor.visibleProfiles"' in modal
247
+ assert "$store.agentEditor.activeProfile().id !== 'default'" in modal
248
assert 'name="palette"' in modal and 'name="add_photo_alternate"' in modal
218
- assert ".easy-tool-summary" not in modal
219
- easy_tool_list_style = re.search(
220
- r"\.easy-tool-list\s*\{([^}]*)\}", modal
249
+ assert ".capability-accordion .policy-items { max-height:none; overflow:visible;" in modal
250
+ assert ".capability-policy-group + .capability-policy-group { border-top:1px solid var(--color-border);" in modal
251
+ assert "font-weight:500" in re.search(
252
+ r"\.capability-accordion summary\s*\{([^}]*)\}", modal
253
).group(1)
222
- assert "max-height" not in easy_tool_list_style
223
- assert "overflow-y" not in easy_tool_list_style
224
- assert "grid-template-columns:minmax(0,1fr) 2.5rem minmax(0,1fr)" in modal
225
- assert ".policy-lists .policy-transfer-actions { flex-direction:row; }" in modal
226
- assert "easyToolsOpen" not in store_source
227
- assert "easySkills" not in store_source
228
- assert "get toolMode" not in store_source
229
- assert "get easyTools" not in store_source
230
- assert "firstSentence" not in store_source
254
+ assert ".capability-accordion[open] summary x-icon { transform:rotate(180deg); }" in modal
255
+ assert "toolCategory" not in store_source
256
+ assert "selectedAllowed" not in store_source and "selectedBlocked" not in store_source
257
+ assert "moveAllVisible" not in store_source and "confirmBulkMove" not in store_source
258
+ assert "setPolicyItemState" in store_source and "collapsePolicy" in store_source
259
assert '.agent-editor [aria-invalid="true"]' not in modal
260
assert "color-scheme:dark" not in modal
261
assert "#fff 82%" not in modal
262
assert ".agent-manager-name strong,.agent-manager-copy p { overflow-wrap:anywhere; }" in modal
263
assert ".field-error { display:block; color:var(--color-text)" in modal
236
- assert ".prompt-pane { min-width:0; min-height:0;" in modal
264
+ assert ".prompt-pane" not in modal
265
assert 'callJsonApi("/plugins/_agent_editor/agent_editor"' in switcher_mixin
266
assert "profile.enabled !== false" in switcher_mixin
267
assert 'x-show="!$store.modelConfig.agentProfilesLoading"' in switcher
268
assert "@keydown.ctrl.s.prevent" in modal
269
assert "@media (max-width: 760px)" in modal
242
- assert modal.count('<label class="policy-item"><input type="checkbox"') == 4
243
- assert '<div class="policy-item"><input type="checkbox"' not in modal
270
+ assert "tri-state-item" not in modal
271
+ assert "policy-state-legend" not in modal
272
+ assert "policy-item-state" not in modal
273
+ assert modal.count("Default (${$store.agentEditor.policyDefault") == 6
274
+ assert "border-radius:var(--border-radius-sm)" in modal
275
+ assert "cyclePolicyItem" not in store_source and "policyAriaChecked" not in store_source
276
assert ".agent-manager-card { grid-template-columns:3rem minmax(0,1fr) auto; align-items:start; }" in modal
277
assert ".agent-manager-actions { grid-column:auto; align-self:stretch; display:grid;" in modal
278
assert ".agent-manager-actions .agent-profile-availability { grid-column:1/-1; }" in modal
@@ -309,12 +341,35 @@ const chatsStore = {
341
};
342
const modelConfigStore = {
343
loadAgentProfiles: async force => calls.push({ endpoint: "loadAgentProfiles", payload: force }),
344
+ openPresetEditor: async preset => calls.push({ endpoint: "openPresetEditor", payload: preset }),
345
selectAgentProfile: async (contextId, profileId) => {
346
calls.push({ endpoint: "selectAgentProfile", payload: { contextId, profileId } });
347
return true;
348
},
349
getAgentProfileVisual: (_id, label) => ({ color: "#123456", url: "", initials: label?.[0] || "A" }),
350
};
351
+const aceState = { change: null, find: null, destroyed: false, value: "" };
352
+const aceContainer = { textContent: "" };
353
+const aceSession = {
354
+ setMode: value => { aceState.mode = value; },
355
+ setUseWrapMode: value => { aceState.wrap = value; },
356
+ on: (name, callback) => { if (name === "change") aceState.change = callback; },
357
+ off: (name, callback) => { if (name === "change" && aceState.change === callback) aceState.change = null; },
358
+};
359
+const aceEditor = {
360
+ container: aceContainer,
361
+ session: aceSession,
362
+ textInput: { getElement: () => ({ setAttribute: (key, value) => { aceState[key] = value; } }) },
363
+ setTheme: value => { aceState.theme = value; },
364
+ setOptions: value => { aceState.options = value; },
365
+ setValue: value => { aceState.value = value; aceState.change?.(); },
366
+ getValue: () => aceState.value,
367
+ find: (query, options) => { aceState.find = { query, options }; },
368
+ focus: () => { aceState.focused = true; },
369
+ resize: () => { aceState.resized = true; },
370
+ destroy: () => { aceState.destroyed = true; },
371
+};
372
+globalThis.ace = { edit: container => { aceState.container = container; return aceEditor; } };
373
globalThis.window = globalThis;
374
globalThis.document = {
375
dispatchEvent: (event) => calls.push({ endpoint: "event", payload: event.type }),
@@ -350,13 +405,14 @@ store.state = {
405
{ filename: "agent.system.main.communication.md", group: "2.4", group_label: "Communication", effective: "Inherited comm", inherited: "Inherited comm", source_chain: ["Framework", "Researcher"], state: "Inherited", has_override: false },
406
],
407
model_preset: { has_override: false },
353
- tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [] },
408
+ tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [] },
409
skills: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [] },
410
};
411
store.makeDraft(true);
412
if (await store.previewPlan()) throw new Error("invalid plan unexpectedly succeeded");
413
if (store.planStatus !== "blocked" || store.error || store.validationIssues().length !== 2) throw new Error("blocked plan state mismatch");
414
if (store.fieldIssue("name")?.message !== "Agent name is required.") throw new Error("inline name issue missing");
415
+if (store.fieldIssue("instructions")?.field !== "agent-editor-instructions") throw new Error("Easy validation targeted the wrong input");
416
await store.save();
417
if (store.error) throw new Error("validation leaked into dismissible error banner");
418
store.state.profile.id = "new-agent";
@@ -365,7 +421,8 @@ store.state.profile.metadata.title = { inherited_source: "agents/new-agent/agent
421
if (store.metadataProvenance("title") !== "") throw new Error("new profile showed misleading provenance");
422
store.draft.creating = false;
423
if (store.metadataProvenance("title") !== "Using the default") throw new Error("default provenance mismatch");
368
-store.profiles = [{ id: "researcher", title: "Researcher" }];
424
+store.profiles = [{ id: "researcher", title: "Researcher" }, { id: "default", title: "Default" }];
425
+if (store.visibleProfiles.length !== 1 || store.visibleProfiles[0].id !== "researcher") throw new Error("Default profile remained selectable in Agent Editor");
426
store.state.profile.metadata.title = { inherited_source: "agents/researcher/agent.yaml" };
427
if (store.metadataProvenance("title") !== "Inherited from Researcher") throw new Error("inherited provenance mismatch");
428
store.state.profile.metadata.title.has_override = true;
@@ -378,35 +435,50 @@ if (store.promptCustomizationPath() !== "usr/agents/researcher/prompts/agent.sys
435
store.projectName = "demo";
436
if (store.promptCustomizationPath() !== "usr/projects/demo/.a0proj/agents/researcher/prompts/agent.system.main.specifics.md") throw new Error("project prompt customization path mismatch");
437
store.projectName = "";
438
+store.state.model_preset.effective = "Current";
439
+store.state.model_presets = [{ name: "Codex" }];
440
+store.draft.modelPreset = "Codex";
441
+if (store.buildPatch().model_preset?.name !== "Codex") throw new Error("Easy model preset was not saved");
442
+loadHandler = () => ({ ok: true, state: { model_presets: [{ name: "Codex" }] } });
443
+await store.openPresetManager();
444
+loadHandler = null;
445
+if (!calls.some(call => call.endpoint === "openPresetEditor" && call.payload === "Codex")) throw new Error("Easy preset editor action did not reuse Model Configuration");
446
+store.draft.modelPreset = "";
447
store.state.tools.catalog = [
448
{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
449
{ id: "local:gone", name: "gone", label: "Gone", origin: "Unavailable", available: false },
450
+ { id: "mcp:docs:read", name: "read", label: "Docs read", origin: "MCP", available: true },
451
];
385
-store.draft.toolPolicy = { mode: "custom", default: "allow", allowed: [], blocked: ["local:shell"] };
452
+store.draft.toolPolicy = { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] };
453
+store.initialDraft.toolPolicy = clone(store.draft.toolPolicy);
454
+if (store.standardToolCatalog.length !== 1 || store.mcpCatalog.length !== 1 || store.toolCatalog.length !== 2) throw new Error("Easy tool/MCP grouping mismatch");
455
+if (store.filteredTools("tool").length !== 2 || store.filteredTools("mcp").length !== 1) throw new Error("Advanced retained catalog grouping mismatch");
456
+if (store.policyItemState("tool", "local:shell") !== "default") throw new Error("initial segmented state mismatch");
457
+store.setPolicyItem("tool", "local:shell", "allow");
458
+if (store.policyItemState("tool", "local:shell") !== "allow" || !store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("On selection failed");
459
+store.setPolicyItem("tool", "local:shell", "block");
460
+if (store.policyItemState("tool", "local:shell") !== "block" || !store.draft.toolPolicy.blocked.includes("local:shell")) throw new Error("Off selection failed");
461
if (JSON.stringify(store.skillWarnings({ allowed_tools: ["shell"] })) !== JSON.stringify(["shell"])) throw new Error("live skill warning missing");
387
-if (store.toolCatalog.length !== 1 || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("Easy custom tool state mismatch");
388
-await store.moveAllVisibleTools(true);
389
-if (confirmations.at(-1)?.title !== "Allow 1 shown tool?" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("filtered bulk confirmation mismatch");
390
-confirmations.length = 0;
391
-store.selectedAllowedTools = ["local:shell"];
392
-store.useStandardTools();
393
-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");
394
-store.setEasyToolAllowed("local:shell", false);
395
-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");
396
-store.setEasyToolAllowed("local:shell", true);
397
-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");
398
-store.draft.toolPolicy = { mode: "custom", default: "block", allowed: [], blocked: [] };
399
-store.setEasyToolAllowed("local:shell", true);
400
-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");
401
-store.setEasyToolAllowed("local:shell", false);
402
-if (store.isToolAllowed(store.state.tools.catalog[0]) || store.draft.toolPolicy.allowed.length) throw new Error("Easy uncheck ignored block-by-default policy");
403
-store.useStandardTools();
404
-store.chooseTools();
405
-if (store.draft.toolPolicy.mode !== "custom" || store.draft.toolPolicy.default !== "allow" || store.section !== "3") throw new Error("custom tool editor did not open");
406
-store.useStandardTools();
407
-store.state.tools.effective_policy = { mode: "inherit", default: "block", allowed: [], blocked: ["local:shell"] };
408
-store.chooseTools();
409
-if (store.draft.toolPolicy.default !== "allow" || store.draft.toolPolicy.blocked.length) throw new Error("inactive inherited exceptions leaked into custom policy");
462
+store.setPolicyItem("tool", "local:shell", "default");
463
+if (store.draft.toolPolicy.mode !== "inherit" || store.policyItemState("tool", "local:shell") !== "default") throw new Error("segmented undo did not collapse to inherit");
464
+store.setPolicyDefault("tool", "block");
465
+store.setPolicyItem("tool", "local:shell", "allow");
466
+store.setPolicyDefault("tool", "allow");
467
+if (!store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("explicit On was lost when the default changed");
468
+store.setPolicyItem("tool", "local:shell", "block");
469
+store.setPolicyDefault("tool", "block");
470
+if (!store.draft.toolPolicy.blocked.includes("local:shell")) throw new Error("explicit Off was lost when the default changed");
471
+store.setPolicyItem("tool", "local:shell", "default");
472
+store.setPolicyDefault("tool", "allow");
473
+if (store.draft.toolPolicy.mode !== "inherit") throw new Error("default undo did not collapse to inherit");
474
+store.setPolicyDefault("mcp", "block");
475
+if (store.policyDefault("tool") !== "allow" || store.policyDefault("mcp") !== "block") throw new Error("tool and MCP defaults were not independent");
476
+if (!store.isToolAllowed(store.state.tools.catalog[0]) || store.isToolAllowed(store.state.tools.catalog[2])) throw new Error("MCP default affected the wrong catalog group");
477
+store.setPolicyItem("mcp", "mcp:docs:read", "allow");
478
+store.setPolicyDefault("mcp", "allow");
479
+if (!store.draft.toolPolicy.allowed.includes("mcp:docs:read")) throw new Error("explicit MCP On was lost when its default changed");
480
+store.setPolicyItem("mcp", "mcp:docs:read", "default");
481
+if (store.draft.toolPolicy.mode !== "inherit") throw new Error("MCP default undo did not collapse to inherit");
482
store.projectName = "demo";
483
store.intent = { ...store.intent, projectName: "demo" };
484
if (!store.currentChatUsesScope() || !store.isProfileActive("researcher")) throw new Error("active project profile state mismatch");
@@ -468,26 +540,29 @@ confirmResult = false;
540
store.projectName = "other";
541
if (store.currentChatUsesScope() || store.isProfileActive("default")) throw new Error("foreign project profile appeared active");
542
store.projectName = "demo";
471
-store.state.tools.effective_policy = { mode: "custom", default: "allow", allowed: [], blocked: ["local:shell"] };
472
-store.useStandardTools();
543
+store.state.tools.effective_policy = { mode: "custom", default: "allow", mcp_default: "allow", allowed: [], blocked: ["local:shell"] };
544
if (store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope ignored inherited tool restriction");
474
-store.setEasyToolAllowed("local:shell", true);
545
+store.setPolicyItem("tool", "local:shell", "default");
546
if (store.draft.toolPolicy.mode !== "custom" || !store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope did not customize inherited policy");
476
-store.setEasyToolAllowed("local:shell", false);
547
+store.setPolicyItem("tool", "local:shell", "allow");
548
+if (!store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("project scope did not pin explicit On");
549
+store.setPolicyItem("tool", "local:shell", "block");
550
if (store.draft.toolPolicy.mode !== "inherit" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope did not restore inherited policy");
551
store.projectName = "";
479
-store.state.tools.effective_policy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
552
+store.state.tools.effective_policy = { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] };
553
store.state.skills.catalog = [
554
{ name: "Research", path: "skills/research/SKILL.md", origin: "Agent Zero", description: "Research sources", available: true, tags: [], allowed_tools: [] },
555
{ name: "Gone", path: "skills/gone/SKILL.md", origin: "Unavailable", description: "Missing skill", available: false, tags: [], allowed_tools: [] },
556
];
484
-store.draft.skillPolicy = { mode: "custom", default: "allow", allowed: [], blocked: ["Research"] };
485
-if (store.skillCatalog.length !== 1 || store.filteredSkills(false).length !== 1 || store.filteredSkills(true).length !== 1) throw new Error("custom skill catalog mismatch");
486
-store.selectedBlockedSkills = ["Research"];
487
-store.useStandardSkills();
488
-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");
489
-store.chooseSkills();
490
-if (store.draft.skillPolicy.mode !== "custom" || store.draft.skillPolicy.default !== "allow") throw new Error("custom skill editor did not open");
557
+store.draft.skillPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
558
+store.initialDraft.skillPolicy = clone(store.draft.skillPolicy);
559
+if (store.skillCatalog.length !== 1 || store.filteredSkills().length !== 2) throw new Error("Easy/Advanced skill catalog mismatch");
560
+store.setPolicyItem("skill", "Research", "allow");
561
+if (!store.draft.skillPolicy.allowed.includes("Research")) throw new Error("skill On selection failed");
562
+store.setPolicyItem("skill", "Research", "block");
563
+if (!store.draft.skillPolicy.blocked.includes("Research")) throw new Error("skill Off selection failed");
564
+store.setPolicyItem("skill", "Research", "default");
565
+if (store.draft.skillPolicy.mode !== "inherit") throw new Error("skill sparse undo did not collapse");
566
store.draft.title = "Preserved Agent";
567
store.onNameInput();
568
store.instructions.value = "Preserved instructions";
@@ -499,6 +574,7 @@ store.mode = "advanced";
574
store.instructions.value = "";
575
const callsBeforeInvalidAdvancedCreate = calls.length;
576
if (!store.fieldIssue("instructions")) throw new Error("Advanced create accepted empty instructions");
577
+if (store.fieldIssue("instructions")?.field !== "agent-editor-prompt-ace") throw new Error("Advanced validation targeted the wrong editor");
578
await store.save();
579
if (calls.length !== callsBeforeInvalidAdvancedCreate) throw new Error("Advanced create submitted empty instructions");
580
store.instructions.value = "Preserved instructions";
@@ -525,7 +601,7 @@ store.state = {
601
],
602
model_preset: { has_override: false, effective: "Default" },
603
model_presets: [],
528
- tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
604
+ tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
605
{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
606
{ id: "local:old", name: "old", label: "Old", origin: "Old scope", available: true },
607
] },
@@ -537,6 +613,25 @@ store.state = {
613
store.view = "editor";
614
store.intent = { ...store.intent, view: "create", projectName: "" };
615
store.makeDraft(true);
616
+store.root = {
617
+ querySelector: selector => selector === "#agent-editor-prompt-ace" ? aceContainer : null,
618
+ contains: node => node === aceContainer,
619
+ closest: () => null,
620
+};
621
+store.mode = "advanced";
622
+store.section = "2";
623
+const draftBeforeAce = JSON.stringify(store.draft);
624
+store.initPromptEditor();
625
+if (aceState.mode !== "ace/mode/markdown" || aceState.wrap !== true || aceState.options?.showPrintMargin !== false || aceState.options?.useWorker !== false) throw new Error("ACE configuration mismatch");
626
+if (aceState["aria-label"] !== "Prompt Markdown" || JSON.stringify(store.draft) !== draftBeforeAce) throw new Error("ACE initialization created a false edit");
627
+aceState.value = "Edited in ACE";
628
+aceState.change();
629
+if (store.instructions.value !== "Edited in ACE" || !store.promptEditPending(store.instructions)) throw new Error("ACE change did not update the prompt draft");
630
+store.selectPrompt("agent.system.main.communication.md");
631
+if (aceState.value !== "Old comm" || store.instructions.value !== "Edited in ACE") throw new Error("ACE file switch lost a draft");
632
+store.selectPrompt("agent.system.main.specifics.md");
633
+store.destroyPromptEditor();
634
+if (!aceState.destroyed || aceState.change) throw new Error("ACE instance was not destroyed cleanly");
635
store.draft.title = "Scoped Agent";
636
store.onNameInput();
637
store.draft.description = "Scoped description";
@@ -546,10 +641,9 @@ store.markPromptSet("agent.system.main.specifics.md");
641
store.draft.prompts["agent.system.main.communication.md"].value = "Authored communication";
642
store.acceptPromptEdit("agent.system.main.communication.md");
643
store.chooseAvatarColor("#ABCDEF");
549
-store.setEasyToolAllowed("local:shell", false);
644
+store.setPolicyItem("tool", "local:shell", "block");
645
store.setPolicyDefault("tool", "block");
551
-store.chooseSkills();
552
-store.moveSkills(["Research"], false);
646
+store.setPolicyItem("skill", "Research", "block");
647
const projectState = {
648
profile: { id: "new-agent", avatar_url: "", metadata: { title: {}, description: {}, context: {}, avatar: { effective: { kind: "color", value: "#222222" } } } },
649
prompts: [
@@ -558,7 +652,7 @@ const projectState = {
652
],
653
model_preset: { has_override: false, effective: "Default" },
654
model_presets: [],
561
- tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
655
+ tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
656
{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
657
{ id: "local:new", name: "new", label: "New", origin: "Project", available: true },
658
] },
@@ -569,7 +663,7 @@ const projectState = {
663
};
664
loadHandler = () => ({ ok: true, state: projectState });
665
store.mode = "advanced";
572
-store.section = "5";
666
+store.section = "6";
667
store.projectName = "demo";
668
store.intent = { ...store.intent, projectName: "" };
669
calls.length = 0;
@@ -577,7 +671,7 @@ await store.onScopeChanged();
671
if (store.state !== projectState || store.draft.title !== "Scoped Agent" || store.draft.profileId !== "scoped-agent") throw new Error("create scope rebase lost identity");
672
if (store.draft.description !== "Scoped description" || store.draft.context !== "Use for scoped work") throw new Error("create scope rebase lost authored metadata");
673
if (store.instructions.value !== "Authored instructions" || store.instructions.source !== "project-source") throw new Error("create scope rebase kept stale prompt provenance");
580
-if (store.draft.avatar?.value !== "#ABCDEF" || store.isToolAllowed(projectState.tools.catalog[0]) || !store.isToolAllowed(projectState.tools.catalog[1]) || store.draft.toolPolicy.default !== "block") throw new Error("create scope rebase lost avatar or explicit tool decision");
674
+if (store.draft.avatar?.value !== "#ABCDEF" || store.isToolAllowed(projectState.tools.catalog[0]) || store.isToolAllowed(projectState.tools.catalog[1]) || store.draft.toolPolicy.default !== "block") throw new Error("create scope rebase lost avatar or tool policy");
675
if (store.isSkillAllowed(projectState.skills.catalog[0]) || !store.isSkillAllowed(projectState.skills.catalog[1])) throw new Error("create scope rebase lost explicit skill decision");
676
if (store.draft.prompts["agent.system.main.communication.md"].value !== "Authored communication" || store.draft.prompts["agent.system.main.communication.md"].source !== "project-source" || store.promptEditPending(store.draft.prompts["agent.system.main.communication.md"])) throw new Error("create scope rebase lost an accepted prompt edit or kept stale provenance");
677
if (calls.at(-1)?.payload?.action !== "plan" || calls.at(-1)?.payload?.project_name !== "demo" || store.planStatus !== "ready" || !store.plan.written[0].startsWith("usr/projects/demo/.a0proj/agents/scoped-agent/")) throw new Error("Review plan was not recomputed after scope change");
@@ -602,14 +696,13 @@ store.onPromptInput(communication.filename);
696
store.acceptPromptEdit(communication.filename);
697
if (store.promptEditPending(communication)) throw new Error("prompt edit was not accepted");
698
if (store.promptDisplayState(communication) !== "Customized by you") throw new Error("customized state missing");
605
-if (store.promptSourceChain(communication) !== "Customized by you") throw new Error("customized provenance missing");
699
store.resetPrompt(communication.filename);
700
if (store.promptEditPending(communication) || store.promptDisplayState(communication) !== "Will use the default") throw new Error("reset state mismatch");
701
const draftBeforeModes = JSON.stringify(store.draft);
702
store.setMode("advanced", "2");
703
store.setMode("easy");
704
if (store.section !== "2" || JSON.stringify(store.draft) !== draftBeforeModes) throw new Error("mode switch lost draft");
612
-store.setMode("advanced", "5");
705
+store.setMode("advanced", "6");
706
await Promise.resolve();
707
await Promise.resolve();
708
if (store.planStatus !== "ready" || calls.at(-1).payload.action !== "plan") throw new Error("review plan was not computed on entry");
@@ -634,7 +727,7 @@ if (calls.length || !store.error.includes("Save or discard")) throw new Error("d
727
store.initialDraft = clone(store.draft);
728
store.error = "";
729
await store.planRemoval(true);
637
-if (!store.pendingMutation?.destructive || store.section !== "5" || store.planStatus !== "ready") throw new Error("removal plan was replaced");
730
+if (!store.pendingMutation?.destructive || store.section !== "6" || store.planStatus !== "ready") throw new Error("removal plan was replaced");
731
if (calls.at(-1).payload.action !== "plan_remove_changes") throw new Error("removal plan request missing");
732
if (calls.at(-1).payload.project_name !== "demo") throw new Error("removal request lost selected scope");
733
const callsBeforePendingSave = calls.length;
@@ -704,9 +797,13 @@ const store = { ...switcherState, ...switcherMethods };
797
const older = store.loadAgentProfiles(true);
798
const newer = store.loadAgentProfiles(true);
799
if (pending.length !== 2 || !store.agentProfilesLoading) throw new Error("overlapping profile loads did not start");
707
-pending[1]({ profiles: [{ id: "new", title: "New", enabled: true }] });
800
+pending[1]({ profiles: [
801
+ { id: "default", title: "Default", enabled: true },
802
+ { id: "new", title: "New", enabled: true },
803
+] });
804
await newer;
805
if (store.agentProfiles[0]?.key !== "new" || store.agentProfilesLoading || !store.agentProfilesLoaded) throw new Error("newest profile load did not settle");
806
+if (store.agentProfiles.length !== 1 || store.getAgentProfileList("default", "Default").some(profile => profile.key === "default")) throw new Error("Default profile remained selectable in the chat popover");
807
pending[0]({ profiles: [{ id: "old", title: "Old", enabled: true }] });
808
await older;
809
if (store.agentProfiles[0]?.key !== "new" || store.agentProfilesLoading || !store.agentProfilesLoaded) throw new Error("stale profile load replaced newer state");
tests/test_tool_policy.py
+21
-1
@@ -53,10 +53,11 @@ def _prompt_paths(root: Path):
53
return get_paths
54
55
56
-def _custom_policy(*, default: str, allowed=(), blocked=()):
56
+def _custom_policy(*, default: str, mcp_default: str = "allow", allowed=(), blocked=()):
57
return {
58
"mode": "custom",
59
"default": default,
60
+ "mcp_default": mcp_default,
61
"allowed": list(allowed),
62
"blocked": list(blocked),
63
}
@@ -157,6 +158,25 @@ def test_required_response_survives_default_block(monkeypatch, tmp_path: Path) -
158
assert tool_policy.get_tool_catalog(agent) == []
159
160
161
+def test_tool_and_mcp_defaults_are_independent(monkeypatch, tmp_path: Path) -> None:
162
+ agent = _Agent(tmp_path)
163
+ monkeypatch.setattr(
164
+ tool_policy,
165
+ "get_policy",
166
+ lambda _agent: _custom_policy(
167
+ default="block",
168
+ mcp_default="allow",
169
+ allowed=["local:pinned"],
170
+ blocked=["mcp:docs:delete"],
171
+ ),
172
+ )
173
+
174
+ assert tool_policy.resolve_tool(agent, "shell", canonical_id="local:shell").allowed is False
175
+ assert tool_policy.resolve_tool(agent, "read", canonical_id="mcp:docs:read").allowed is True
176
+ assert tool_policy.resolve_tool(agent, "pinned", canonical_id="local:pinned").allowed is True
177
+ assert tool_policy.resolve_tool(agent, "delete", canonical_id="mcp:docs:delete").allowed is False
178
+
179
+
180
def test_catalog_comes_from_executable_tools_not_prompt_names(
181
monkeypatch, tmp_path: Path
182
) -> None: