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: